Compare commits

...
Author SHA1 Message Date
ohif-bot be995f93b6 chore(release): publish [skip ci]
- @ohif/extension-vtk@1.2.4
 - @ohif/core@2.3.1
 - @ohif/viewer@3.3.4
2019-12-30 20:01:54 +00:00
Danny Brown 5036615e00 chore: throw our error if we encounter one (no swallow) (#1337) 2019-12-30 14:53:39 -05:00
Gustavo André Lelis 2e215aed4c test: 💍 Updating tests after bug fix (#1324)
Closes: #1167
2019-12-20 19:14:07 -05:00
ohif-bot ffd27d6db4 chore(release): publish [skip ci]
- @ohif/extension-cornerstone@2.2.1
 - @ohif/extension-vtk@1.2.3
 - @ohif/viewer@3.3.3
2019-12-20 21:01:26 +00:00
Gustavo André LelisandDanny Brown 6540e36818 fix: 🐛 1241: Make Plugin switch part of ToolbarModule (#1322)
* fix: 🐛 Make Plugin switch part of ToolbarModule

Closes: #1241

* Move setCornerstoneViewport command into cornerstone extension

* Exposing extensionManager into extension's get modules functions

* Remove Plugin switch

* Updating set cornerstonelayout to work on VIEWER context

* Creating a customComponent for 2D MPR button

* Use extensionManager to show or not buttons
Adding isHidden option into toolbarModules

* Make buttons hidden depending on extension registration

* Fix unit test after changes on extensionManager

* Remove workaround for extensionManager

* Use studyMeatadataManager and remove TODO's

* Remove storeContext not needed

* Moving Exit 2D MPR into cornerstone extension with VTK context

* Remove PluginSwitch files

* Fix E2E tests

* Adding cypress screenshots into gitignore

* Remove extensionManager from getModuleFn functions and moving CustomComponent for VTK button to VTK extension

Co-authored-by: Danny Brown <danny.ri.brown@gmail.com>
2019-12-20 15:59:36 -05:00
ohif-bot a436436def chore(release): publish [skip ci]
- @ohif/extension-vtk@1.2.2
 - @ohif/ui@1.1.7
 - @ohif/viewer@3.3.2
2019-12-20 19:10:48 +00:00
Mirna Silva d6d92afbda test: Refactoring toolbar data-cy selectors (#1323) 2019-12-20 14:07:00 -05:00
ohif-bot e1142910e4 chore(release): publish [skip ci]
- @ohif/extension-vtk@1.2.1
 - @ohif/ui@1.1.6
 - @ohif/viewer@3.3.1
2019-12-20 18:57:38 +00:00
Mirna Silva 067e6c769b test: Save Measurements Functionality (#1300)
* E2E Tests: Save Measurements Functionality

* Moving test file to PWA only. Script-tag doesn't have the required Study

* Removing data-cy selector from cornerstone toolbar
2019-12-20 13:54:59 -05:00
ohif-bot d375d02a4b chore(release): publish [skip ci]
- @ohif/extension-cornerstone@2.2.0
 - @ohif/extension-vtk@1.2.0
 - @ohif/core@2.3.0
 - @ohif/viewer@3.3.0
2019-12-20 17:18:56 +00:00
Igor OctavianoandDanny Brown 63594d36b0 feat: 🎸 Configuration so viewer tools can nix handles (#1304)
* feat: 🎸 Configuration so viewer tools can nix handles

Adds a key to cornerstone configuration that allows to toggle on/off
handle rendering

Closes: #1223

* Update extension docs

* Update doc

* docs: include hideHandles configuration in our configuration docs

* chore: default to hiding handles for annotations

* chore: switch to drawHandlesOnHover to mimic legacy viewer functionality

* Flip logic for drawHandlesOnHover; reverse of hideHandles

* Don't hide the handles of ellipse or rectangle roi

* invert check

Co-authored-by: Danny Brown <danny.ri.brown@gmail.com>
2019-12-20 12:15:40 -05:00
ohif-bot 6c160f517f chore(release): publish [skip ci]
- @ohif/extension-vtk@1.1.7
 - @ohif/ui@1.1.5
 - @ohif/viewer@3.2.2
2019-12-19 15:15:49 +00:00
Gustavo André Lelis 23305cec9c fix: 🐛 Fix drag-n-drop of local files into OHIF (#1319)
* fix: 🐛 Fix drag-n-drop of local files into OHIF

Closes: #1307

* CR Update: Centralize dialogs
2019-12-19 10:14:05 -05:00
ohif-bot 36a741f0bc chore(release): publish [skip ci]
- @ohif/extension-vtk@1.1.6
 - @ohif/core@2.2.1
 - @ohif/viewer@3.2.1
2019-12-18 18:29:28 +00:00
Igor Octaviano 695ad732e1 refactor: 💡 Move DICOMSR code to @ohif/core project (#1310)
* Move DICOMSR to @ohif/core

* refactor: 💡 Move DICOMSR code to @ohif/core project

We've just merged support for DICOM Structured Reports. Today, support
for this feature lives in the @ohif/viewer project in it's lib folder.
Ideally, it should be moved to @ohif/core and given a clear public API
and unit tests.

Closes: #1282

* Update imports

* CR Update: Update imports
2019-12-18 13:25:53 -05:00
ohif-bot 73a3f07e2b chore(release): publish [skip ci]
- @ohif/extension-vtk@1.1.5
 - @ohif/core@2.2.0
 - @ohif/viewer@3.2.0
2019-12-16 19:11:59 +00:00
Igor Octaviano 4ea239a953 feat: 🎸 Expose extension config to modules (#1279)
* feat: 🎸 Expose extension config to modules

Currently, only the preRegistration hook receives the extension's
configuration as a parameter. Providing getModuleFn's with the
extension's configuration, and all lifecycle/modules with the
application's configuration as rootConfig should open the doors to more
configurable extensions.

Closes: #1268

* CR Update: Pass extension and service config through extension manager preinit/getmodule

* CR Update: Remove appConfig from servicesManager

* CR Update: Remove appconfig variable
2019-12-16 14:07:53 -05:00
ohif-bot 625d5d70a7 chore(release): publish [skip ci]
- @ohif/viewer@3.1.12
2019-12-16 18:30:12 +00:00
Igor Octaviano 5c3d8b37b6 fix: 🐛 Dismiss all dialogs if leaving viewer route #1242 (#1301)
* fix: 🐛 #1242

Dismiss all dialogs if route change

Closes: #1242

* CR Update: Dismiss dialogs in videwer
2019-12-16 13:26:31 -05:00
ohif-bot 8bbbf3a889 chore(release): publish [skip ci]
- @ohif/extension-cornerstone@2.1.1
 - @ohif/viewer@3.1.11
2019-12-16 16:49:50 +00:00
arthur-miller d5bf72851a fix: 🐛 add WwwcRegionTool to cornerstone tools initialization (#1302) 2019-12-16 11:47:02 -05:00
ohif-bot 2914c83032 chore(release): publish [skip ci]
- @ohif/extension-dicom-pdf@1.0.1
 - @ohif/viewer@3.1.10
2019-12-16 16:13:00 +00:00
Igor Octaviano 31e542dfd9 fix: 🐛 Update ohif pdf extension to optionally use pdfjs (#1162)
* fix: 🐛 Update ohif pdf extension to optionally use pdfjs

Update ohif pdf extension of optionally use pdfjs

Closes: #1049

* Update branch and fix scrolling

* Dynamically import component

* Fix wrong file change

* Fix import

* Add multi opened pdf configuration

* Refactor connected component

* Specify worker explicitly

* Specify worker explicitly

* CR Update: Remove dead code

* CR Update: Refactor state

* CR Update: use refs

* CR Update: use refs
2019-12-16 11:10:10 -05:00
ohif-bot 25599a2e2a chore(release): publish [skip ci]
- @ohif/i18n@0.52.4
 - @ohif/viewer@3.1.9
2019-12-16 15:13:14 +00:00
Danny Brown 1cf7015511 docs: stub environment variables docs 2019-12-16 09:47:12 -05:00
Igor Octaviano 756f70cc54 refactor: 💡 Use env variables to set locize (#1299)
Instead of relying in dynamic configuration, use dedicated env variables
to set locize api key / configuration

Closes: #1271
2019-12-16 09:41:41 -05:00
ohif-bot ded2e61913 chore(release): publish [skip ci]
- @ohif/extension-vtk@1.1.4
 - @ohif/core@2.1.1
 - @ohif/ui@1.1.4
 - @ohif/viewer@3.1.8
2019-12-16 14:28:33 +00:00
Igor Octaviano b58aa4575a refactor: Remove LabelingFlow/ContextMenu services and reimplement on top of the Dialog service #1264 (#1292)
* Remove ContextMenu and Labelling services and provider to use DialogService directly

* Refactor init to display labelling

* Cleanup labelling flow in measurements init

* Cleanup viewer imports

* Add classname to toolcontextmenu

* Add error message
2019-12-16 09:19:40 -05:00
Danny Brown f78abe2afa docs: stub viewer configuration 2019-12-16 09:15:10 -05:00
Danny Brown f48157117f docs: hide empty pages 2019-12-16 09:00:24 -05:00
Danny Brown 6f8bd0dff8 docs: fix scope of diagram path 2019-12-16 08:57:43 -05:00
ohif-bot 943af89df2 chore(release): publish [skip ci]
- @ohif/extension-vtk@1.1.3
 - @ohif/ui@1.1.3
 - @ohif/viewer@3.1.7
2019-12-13 15:14:42 +00:00
Danny Brown cd2da349e5 fix: allow empty values for dimensions (#1295)
* fix: allow empty values for dimensions

* fix: consistently use `defaultSize` for intial values
2019-12-13 10:09:18 -05:00
ohif-bot 14e86dcddd chore(release): publish [skip ci]
- @ohif/viewer@3.1.6
2019-12-13 04:45:56 +00:00
Danny Brown 2c8b93656d ci: fix webgl e2e test failures (only on CI) (#1296)
* ci: update executor and browser flag for circleci orbs

* ci: browser flag when running percy/ci tests

* ci: limit ourselves to the executors defined by the cypress orb for now
2019-12-12 23:43:22 -05:00
ohif-bot 6932468206 chore(release): publish [skip ci]
- @ohif/viewer@3.1.5
2019-12-13 02:19:44 +00:00
Danny Brown 7bc9efb0f5 ci: store cypress videos as artifacts; make sure we upload them to dashboard
* fix: Set VTK viewport as active by interaction

* chore: upgrade cypress to latest

* Revert "fix: Set VTK viewport as active by interaction"

This reverts commit b6e174c2def9d8f688a54d32053679788b9aaf76.

* ci: add record flag for master cypress runs

* ci: simplify cypress orb tasks now that cypress@3.8 uses electron w/ chromium 78

* ci: remove video false flag

* ci: drop browser flag

* ci: store video artifacts
2019-12-12 21:17:35 -05:00
ohif-bot 5d47cc43c1 chore(release): publish [skip ci]
- @ohif/viewer@3.1.4
2019-12-13 01:26:39 +00:00
Danny Brown e46a11a01e ci: upgrade cypress version (#1293)
* fix: Set VTK viewport as active by interaction

* chore: upgrade cypress to latest

* Revert "fix: Set VTK viewport as active by interaction"

This reverts commit b6e174c2def9d8f688a54d32053679788b9aaf76.

* ci: add record flag for master cypress runs
2019-12-12 20:21:27 -05:00
ohif-bot 27a950d956 chore(release): publish [skip ci]
- @ohif/viewer@3.1.3
2019-12-12 18:11:14 +00:00
Danny Brown 03d4c10adc chore: remove unused file 2019-12-12 13:08:30 -05:00
Mirna Silva 7a0c369006 test: PT/CT json, studies, series, frames, metadatas (#1145)
* Tests for routes: PT/CT json, studies, series, frames, metadatas

* Fix based on reviews and JSON file for PT/CT Study added

* chore: revert to remote JSON file

* chore: delete PTCTStudy.json file

We can circle back to this approach when/if we have issues. No need to hold-up this PR. My fault on this one.
2019-12-12 01:05:45 -05:00
ohif-bot 8c946a4742 chore(release): publish [skip ci]
- @ohif/extension-vtk@1.1.2
 - @ohif/i18n@0.52.3
 - @ohif/ui@1.1.2
 - @ohif/viewer@3.1.2
2019-12-12 05:54:34 +00:00
Rodrigo Antinarelli 30b9e44220 fix: translations (#1234) 2019-12-12 00:46:58 -05:00
ohif-bot 380abe2705 chore(release): publish [skip ci]
- @ohif/extension-vtk@1.1.1
 - @ohif/ui@1.1.1
 - @ohif/viewer@3.1.1
2019-12-11 16:48:26 +00:00
Mirna Silva f4570c96c2 tests: Download Image File Modal (#1219)
* E2E Tests: Download Image File Modal

* CI fix

* Fixes for PR comments

* Small change just to check CI behavior

* Fix for CI failure: comparing classes instead of image src

* Fixed Download button test and blocked Download Dialog event

* Deleting file ConnectedViewportDownloadForm.js

This file was not supposed to be created by this PR. It was already deleted by another user on Master branch.

* Removing wrong import line

* Fixes based on PR comments

* Fixes based on PR comments

* test: remove assignment to window
2019-12-11 11:46:10 -05:00
ohif-bot 1fe951a71e chore(release): publish [skip ci]
- @ohif/extension-cornerstone@2.1.0
 - @ohif/extension-dicom-html@1.1.0
 - @ohif/extension-vtk@1.1.0
 - @ohif/core@2.1.0
 - @ohif/ui@1.1.0
 - @ohif/viewer@3.1.0
2019-12-11 16:18:54 +00:00
Gustavo André Lelis ebe1af8d4f feat: 🎸 DICOM SR STOW on MeasurementAPI (#954)
* feat: 🎸 DICOM SR STOW on MeasurementAPI

Closes: #758

* feat: 🎸 Adding the dataExchance from DICOM SR

* feat: 🎸 Fix some issues and get the retrieve measurements working

* feat: 🎸 Preventing measurement tool helpers to broke because the data is not Number (it comes as string from dcmjs)

* feat: 🎸 Adding a button to start testing SATE functionality

* feat: 🎸Small fixes into the code

* feat: 🎸 MAke sure we get latest SR in the study, even on lazyLoad mode

* Revert "feat: 🎸 Preventing measurement tool helpers to broke because the data is not Number (it comes as string from dcmjs)"

This reverts commit 4b3a1efb5810487d2319826fd59c03d779882b5e.

* feat: 🎸 Make sure after sync we update the viewports and also that annotation tools are set to passive, so it can be drawn in the viewports

* feat: 🎸 Remove console logs and start using OHIF.log

* feat: 🎸 Refactor measurement table to use Snackbar service of messages and make sure we only show save button if server type is dicomWeb

* feat: 🎸 Refactor to remove mocked wadoRoot and use server

* Fix to expose error once we can't save the SR

* Update DCMJS into latest dev version

* Update dcmjs with latest fixes

* Fix PR callouts

* Logging error on MeasurementReport creation

* Preventing from breaking on non supported tools and also warning that some toolType is not sopported

* Small error message update

* Expose better the errors and when we have unsopported tools into the measurements

* Renaming variable

* Expose dcmjs tool support check

* Change the way we warn the unsupportedTools

* Adding style to the button

* Small change on button style

* Fix git conflicts

* Fix after merge with latest master

* Fix after merge with master, btn was not clickable

* Fixing callouts

* Adding util functions into files and refactor with JSDocs

* Refactoring code, adding some basic JSDocs

* Bump dcmjs with latest version and fixes

* JSDocs and small refactors

* yarn lock file after dcmjs update

* Remove unused parameters

* Registering a plain copy of server into measurementAPI

* Small refactor

* Start changing from toolType to toolName

* Rename a function to get instanceMetadata

* Adding error if server is not DICOMWeb on retrieve

* Move snackbar outside of UI component and use UINotificationService

* Refactor and JSDocs

* Small fixes on JSDocs (style)

* Remove unused import

* Improve JSDocs on MeasurementApi

* Remove refactor leftover

* Improve JSDocs and propTypes

* JSDocs and small refactors
2019-12-11 11:14:01 -05:00
ohif-bot 5cda5159da chore(release): publish [skip ci]
- @ohif/extension-cornerstone@2.0.2
 - @ohif/viewer@3.0.3
2019-12-11 15:41:29 +00:00
Danny Brown 115a3ec0b0 refactor: remove-labelling-manager
Igor/refactor/1267 remove labelling manager
2019-12-11 10:39:36 -05:00
Danny Brown ac82a111e7 Merge branch 'master' into igor/refactor/1267-remove-labelling-manager 2019-12-11 09:16:18 -05:00
Igor a22fd67e13 Fix config check in extension init 2019-12-11 06:54:41 -03:00
Danny Brown 91f0af058f Merge pull request #1278 from OHIF/dannyrb/ci/visual-test
ci: make sure we're updating the base branch for comparisons (percy)
2019-12-10 23:58:53 -05:00
dannyrb 5a1186388d Merge branch 'dannyrb/ci/visual-test' of github.com:OHIF/Viewers into dannyrb/ci/visual-test 2019-12-10 23:52:54 -05:00
dannyrb 2f0f92c095 ci: make sure we're updating the base branch for comparisons (percy) 2019-12-10 23:45:29 -05:00
Danny Brown 5c686268ab Merge pull request #1274 from OHIF/refactor/igor/1267-remove-labelling-manager
refactor: 💡 Remove LabellingManager and Cleanup LabellingFlow #1267
2019-12-10 23:24:14 -05:00
Danny Brown 974aabc9b2 Merge pull request #1276 from OHIF/dannyrb/ci/visual-test
ci: optional visual test step
2019-12-10 23:12:30 -05:00
Danny Brown 17e3b49b06 Merge branch 'master' into dannyrb/ci/visual-test 2019-12-10 23:08:09 -05:00
dannyrb e3c119489a cI: Better job name for percy 2019-12-10 23:07:54 -05:00
ohif-bot 17447baf44 chore(release): publish [skip ci]
- @ohif/extension-vtk@1.0.2
 - @ohif/core@2.0.2
 - @ohif/viewer@3.0.2
2019-12-11 03:31:18 +00:00
Igor Octaviano daa3ddccdb refactor: 💡 ServicesManager factory functions vs instances (#1272)
* refactor: 💡 ServicesManager factory functions vs instances

Refactor servicesManager to take factory functions instead of instances

Closes: #1265

* Remove testing code
2019-12-10 22:28:23 -05:00
dannyrb c91b6dddd8 ci: optional visual test step 2019-12-10 22:17:21 -05:00
Igor 92548191e4 refactor: 💡 Remove LabellingManager and Clean LabellingFlow
Migrate logic from LabellingManager to LabellingFlow and cleanup
LabellingFlow component

Closes: #1267
2019-12-10 18:31:55 -03:00
Igor 68fcaef19a Cleanup labelling flow 2019-12-10 18:18:37 -03:00
Igor ef611e4849 Refactoring labellingflow to function component 2019-12-10 18:08:51 -03:00
Igor d128fd6bf0 Remove LabellingManager component 2019-12-10 17:41:43 -03:00
Igor 3bdd434488 Cleanup LabellingFlow and LabellingManager components 2019-12-10 17:29:40 -03:00
Danny Brown 29bac8df7a chore: don't mark CI/Tooling tasks as stale 2019-12-10 11:37:39 -05:00
Danny Brown 319c67c389 docs: swap out "always red" badge for viewer-dev.ohif.org badge 2019-12-10 08:57:03 -05:00
Danny Brown b9836f6c71 docs: templates in P_R_T directory can only be set w/ query string; removing 2019-12-09 22:06:01 -05:00
Danny Brown 6a6e2d7df3 docs: shift pr template up a dir 2019-12-09 22:05:05 -05:00
Danny Brown 2f8c408ed2 docs: contributing guidelines 2019-12-09 22:02:29 -05:00
Danny Brown 27910239e0 docs: code of conduct 2019-12-09 22:00:17 -05:00
ohif-bot 719bfd02eb chore(release): publish [skip ci]
- @ohif/extension-cornerstone@2.0.1
 - @ohif/extension-vtk@1.0.1
 - @ohif/core@2.0.1
 - @ohif/ui@1.0.1
 - @ohif/viewer@3.0.1
2019-12-09 18:58:37 +00:00
dannyrb 994818faeb Merge branch 'master' of github.com:OHIF/Viewers into ups-master 2019-12-09 13:55:04 -05:00
dannyrb f06ef53e83 docs: beginnings of SOPClassHandlerModule docs 2019-12-09 13:53:35 -05:00
ohif-bot c7008621e4 chore(release): publish [skip ci]
- @ohif/extension-cornerstone@2.0.0
 - @ohif/extension-vtk@1.0.0
 - @ohif/core@2.0.0
 - @ohif/ui@1.0.0
 - @ohif/viewer@3.0.0
2019-12-09 17:49:51 +00:00
Igor Octaviano 55a580659e feat!: Ability to configure cornerstone tools via extension configuration (#1229)
* Fix ExtensionManager bug and add test to bandaid

* Add tools configuration to extension manager preinit

* Fix reducing of configs

* Merge internal with external configs

* Merge internal with external configs

* Remove dialog from init in measurementstable

* Testing injected configuration

* New way to set config

* Add new prop to dialog provider to allow disabling last position

* Remove code from preinit in cornerstone

* Add new prop to dialog provider to allow disabling last position

* Add centralize to dialogs

* Reorder dialogs when adding them

* Fix draggable styles (cursor)

* Remove repositioning methods from labelling flow and remove overlay from labelling manager

* Fix empty array being set in bringToFront

* Add new command to update table and pass commands manager to modules/preinit hook

* Ad UIContextMenu service / factory

* Use new contextmenu service in measurementspanel extension

* Use dialogs for arrow annotate in default

* Remove positioning funcionality from tool context menu

* Add context menu service

* Pass commandsModule to extension

* Update edit description dialog and simple dialog to position relative

* Remove style code from labelling flow and manager

* Remove eventdata from labelling

* Remove labelling code from measurement init

* Add commandsmanager to provider

* Update contextmenu provider and service

* Add touchstart and mouseclick to hide contextmenu

* Hide labelling if click/touch

* Remove labelling and context menu dead code

* Fix undefined bug if ViewerMain grid has no children

* Fix broken prop on context menu

* Update commandsmodule based on master

* Fix broken configuration

* Update script tag config

* Remove cornerstone from toolcontextmenu

* Remove cornerstone from toolcontextmenu

* Split labelling and context menu providers

* Split labelling and context menu providers

* Update test

* Destructure extensions into new array

* CR Update: Move default arrow config to cornerstone instead of default

* CR Update: Fix app configuration props structure

* CR Update: Fix app configuration prop in script tag and extract commands manager from providers

* CR Update: Create custom providers to use commandsManager

* CR Update: Use services directly in measurementspanel

* CR Update: Pass components to providers

* CR Update: Remove position from dialog

* CR Update: fix dialog prop check

* CR Update: Fix comments

* CR Update: Update documentation

* CR Update: Add test default configuration

* CR Update: Add default empy array to extensions

* CR Update: Update i18n configuration all ot match current function configuration

* CR Update: Add defaults to injected dependencies in configuration and extension configuration

* CR Update: Add defaults to configuration with no args

* Update documentation

* CR Update: Add default for tools

* CR Update: Update config object to i18n

* CR Update: spread defaults

* CR Update: Add tool configuration example to cornerstone extension

* CR Update: Add tool configuration to netlify (testing)

* CR Update: Remove netlify config for tools

* CR Update: Rollback changes to i18n to be fixed later

* CR Update: Update documentation and pass whole cornerstone config object instead of tools key

SEE: https://www.conventionalcommits.org/en/v1.0.0/#commit-message-with-both-and-breaking-change-footer

BREAKING CHANGE: modifies the exposed react <App /> components props. The contract for providing configuration for the app has changed. Please reference updated documentation for guidance.
2019-12-09 12:47:23 -05:00
dannyrb fd7620ced9 docs: add command behaviors to disambiguate situations in which commands may behave unexpectedly 2019-12-09 10:15:06 -05:00
dannyrb cd1cbf0f85 docs: fix GitHub table formatting issue 2019-12-09 09:56:17 -05:00
dannyrb 08055398de docs: commands module docs 2019-12-09 09:52:13 -05:00
dannyrb f6db7fade6 docs: basic viewport docs 2019-12-09 01:02:02 -05:00
dannyrb 58d2bf1d12 test: consistent pre-amble for panel docs 2019-12-09 00:44:43 -05:00
dannyrb 87f5eca659 docs: gif of nested buttons 2019-12-09 00:44:26 -05:00
dannyrb 922ba8cc42 docs: toolbar docs for three different kinds of definitions 2019-12-09 00:44:04 -05:00
dannyrb 777ff88f3d docs: fix incorrect paths 2019-12-08 21:31:00 -05:00
dannyrb 68858db4b6 docs: fixing jsdoc for preregistration example 2019-12-08 21:26:54 -05:00
dannyrb ab106afae3 docs: panel docs and gif 2019-12-08 21:26:39 -05:00
dannyrb ac69b0a656 docs: ExtensionManager class docs 2019-12-08 14:37:54 -05:00
dannyrb 8edba24eec docs: filling in preRegistration hook info 2019-12-08 14:37:42 -05:00
dannyrb aae232fc3e docs: tidying up extensions summary info 2019-12-08 14:09:42 -05:00
155 changed files with 4678 additions and 3424 deletions

No files matched your search

+60 -27
View File
@@ -13,14 +13,7 @@ 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/
cypress: cypress-io/cypress@1.13.0
defaults: &defaults
docker:
@@ -341,6 +334,8 @@ workflows:
post-steps:
- store_artifacts:
path: platform/viewer/cypress/screenshots
- store_artifacts:
path: platform/viewer/cypress/videos
requires:
- UNIT_TESTS
# E2E: script-tag
@@ -364,9 +359,39 @@ workflows:
post-steps:
- store_artifacts:
path: platform/viewer/cypress/screenshots
- store_artifacts:
path: platform/viewer/cypress/videos
requires:
- UNIT_TESTS
PR_OPTIONAL_VISUAL_TESTS:
jobs:
- AWAIT_APPROVAL:
type: approval
# Update hub.docker.org
- cypress/run:
name: 'Generate Percy Snapshots'
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
store_artifacts: false
working_directory: platform/viewer
build: npx cross-env QUICK_BUILD=true yarn run build
# start server --> verify running --> percy + chrome + cypress
command: yarn run test:e2e:dist
cache-key: 'yarn-packages-{{ checksum "yarn.lock" }}'
no-workspace: true # Don't persist workspace
post-steps:
- store_artifacts:
path: platform/viewer/cypress/screenshots
- store_artifacts:
path: platform/viewer/cypress/videos
requires:
- AWAIT_APPROVAL
PR_OPTIONAL_DOCKER_PUBLISH:
jobs:
# https://circleci.com/docs/2.0/workflows/#holding-a-workflow-for-a-manual-approval
@@ -419,25 +444,33 @@ workflows:
filters:
branches:
only: master
# Update base branch snapshots
# and record a Cypress dashboard test run
- cypress/run:
name: 'Generate Percy Snapshots'
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
store_artifacts: false
working_directory: platform/viewer
build: npx cross-env QUICK_BUILD=true yarn run build
# start server --> verify running --> percy + chrome + cypress
command: yarn run test:e2e:dist
cache-key: 'yarn-packages-{{ checksum "yarn.lock" }}'
no-workspace: true # Don't persist workspace
post-steps:
- store_artifacts:
path: platform/viewer/cypress/screenshots
- store_artifacts:
path: platform/viewer/cypress/videos
- store_test_results:
path: platform/viewer/cypress/results
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
@@ -9,7 +9,7 @@ daysUntilClose: 7
exemptLabels:
- 'Story :raised_hands:'
- 'Bug: Verified :bug:'
- 'Task: CI/Tooling 🤖'
- 'Task: CI/Tooling :robot:'
- 'Task: Docs 📖'
- 'Task: Docs :book:'
- 'Task: Refactor 🛠'
+1
View File
@@ -30,6 +30,7 @@ docker/dcm4che/dcm4che-arc
# Cypress test results
videos/
screenshots/
# Locize settings
.locize
+5
View File
@@ -72,12 +72,17 @@ module.exports = (env, argv, { SRC_DIR, DIST_DIR }) => {
},
plugins: [
new webpack.DefinePlugin({
/* Application */
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
'process.env.DEBUG': JSON.stringify(process.env.DEBUG),
'process.env.APP_CONFIG': JSON.stringify(process.env.APP_CONFIG || ''),
'process.env.PUBLIC_URL': JSON.stringify(process.env.PUBLIC_URL || '/'),
'process.env.VERSION_NUMBER': JSON.stringify(PACKAGE.version || ''),
'process.env.BUILD_NUM': JSON.stringify(BUILD_NUM),
/* i18n */
'process.env.USE_LOCIZE': JSON.stringify(process.env.USE_LOCIZE || ''),
'process.env.LOCIZE_PROJECTID': JSON.stringify(process.env.LOCIZE_PROJECTID || ''),
'process.env.LOCIZE_API_KEY': JSON.stringify(process.env.LOCIZE_API_KEY || ''),
}),
],
// Fix: https://github.com/webpack-contrib/css-loader/issues/447#issuecomment-285598881
+76
View File
@@ -0,0 +1,76 @@
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, sex characteristics, gender identity and expression,
level of experience, education, socio-economic status, nationality, personal
appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment
include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at danny.ri.brown+OHIFcoc@gmail.com. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see
https://www.contributor-covenant.org/faq
+1
View File
@@ -0,0 +1 @@
See our contributing guidelines at [`https://docs.ohif.org`](https://docs.ohif.org/development/contributing.html)
+2 -2
View File
@@ -271,8 +271,8 @@ MIT © [OHIF](https://github.com/OHIF)
<!-- Badges -->
[lerna-image]: https://img.shields.io/badge/maintained%20with-lerna-cc00ff.svg
[lerna-url]: https://lerna.js.org/
[netlify-image]: https://api.netlify.com/api/v1/badges/a5d369ab-18a6-41c3-bcde-83805205ac7f/deploy-status
[netlify-url]: https://app.netlify.com/sites/ohif/deploys
[netlify-image]: https://api.netlify.com/api/v1/badges/32708787-c9b0-4634-b50f-7ca41952da77/deploy-status
[netlify-url]: https://app.netlify.com/sites/ohif-dev/deploys
[all-contributors-image]: https://img.shields.io/badge/all_contributors-0-orange.svg?style=flat-square
[circleci-image]: https://circleci.com/gh/OHIF/Viewers.svg?style=svg
[circleci-url]: https://circleci.com/gh/OHIF/Viewers
+1 -2
View File
@@ -1,7 +1,6 @@
# OHIF Viewers
- [Our Process](our-process.md)
- [Quick Start](quick-start.md)
- Development
- [Getting Started](development/getting-started.md)
- [Contributing](development/contributing.md)
@@ -27,10 +26,10 @@
- [Toolbar](extensions/modules/toolbar.md)
- [Viewport](extensions/modules/viewport.md)
- [Contexts](extensions/index.md#contexts)
- [ExtensionManager](extensions/index.md#extensionmanager)
- [OHIF Maintained](extensions/index.md#maintained-extensions)
- [Services](services/index.md)
- [Default](services/default/index.md)
- [Measurements](services/default/measurements.md)
- [UI](services/ui/index.md)
- [Dialog Service](services/ui/ui-dialog-service.md)
- [Modal Service](services/ui/ui-modal-service.md)
Binary file not shown.

After

Width:  |  Height:  |  Size: 178 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

+46
View File
@@ -66,6 +66,52 @@ window.config = {
};
```
The configuration can also be written as a JS Function in case you need to inject dependencies like external services:
```js
window.config = ({ servicesManager } = {}) => {
const { UIDialogService } = servicesManager.services;
return {
cornerstoneExtensionConfig: {
tools: {
ArrowAnnotate: {
configuration: {
getTextCallback: (callback, eventDetails) => UIDialogService.create({...
}
}
},
},
routerBasename: '/',
servers: {
dicomWeb: [
{
name: 'DCM4CHEE',
wadoUriRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/wado',
qidoRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/rs',
wadoRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/rs',
qidoSupportsIncludeField: true,
imageRendering: 'wadors',
thumbnailRendering: 'wadors',
},
],
},
};
};
```
You can also create a new config file and specify its path relative to the build
output's root by setting the `APP_CONFIG` environment variable. You can set the
value of this environment variable a few different ways:
- ~[Add a temporary environment variable in your shell](https://facebook.github.io/create-react-app/docs/adding-custom-environment-variables#adding-temporary-environment-variables-in-your-shell)~
- Previous `react-scripts` functionality that we need to duplicate with
`dotenv-webpack`
- ~[Add environment specific variables in `.env` file(s)](https://facebook.github.io/create-react-app/docs/adding-custom-environment-variables#adding-development-environment-variables-in-env)~
- Previous `react-scripts` functionality that we need to duplicate with
`dotenv-webpack`
- Using the `cross-env` package in an npm script:
- `"build": "cross-env APP_CONFIG=config/my-config.js react-scripts build"`
After updating the configuration, `yarn run build` to generate updated build
output.
@@ -24,12 +24,12 @@ include tags. Here's how it works:
</ul>
<ol start="2">
<li>Create a JS Object to hold the OHIF Viewer's configuration. Here are some
<li>Create a JS Object or Function to hold the OHIF Viewer's configuration. Here are some
example values that would allow the viewer to hit our public PACS:</li>
</ol>
```js
// Set before importing `ohif-viewer`
// Set before importing `ohif-viewer` (JS Object)
window.config = {
// default: '/'
routerBasename: '/',
@@ -49,6 +49,9 @@ window.config = {
};
```
To learn more about how you can configure the OHIF Viewer, check out our
[Configuration Guide](./index.md).
<ol start="3"><li>
Render the viewer in the web page's target <code>div</code>
</li></ol>
+73 -12
View File
@@ -8,12 +8,15 @@
- [Modules](#modules)
- [Contexts](#contexts)
- [Consuming Extensions](#consuming-extensions)
- [Extension Manager](#extensionmanager)
- [Maintained Extensions](#maintained-extensions)
## Overview
We use extensions to help us isolate and package groups of related features.
Extensions provide functionality, ui components, and new behaviors.
Extensions provide functionality, ui components, and new behaviors. Ideally,
they're built in a way that allows them to extend entirely different
implementations of the `@ohif/viewer` project.
<div style="text-align: center;">
<a href="/assets/img/extensions-diagram.png">
@@ -46,9 +49,8 @@ Practical examples of extensions include:
### Extension Skeleton
An extension is a plain JavaScript object has an `id` property, and one or more
"getModuleFunctions" and/or lifecycle hooks. You can read more about
[lifecycle hooks](#lifecycle-hooks) and [modules](#modules) further down.
An extension is a plain JavaScript object that has an `id` property, and one or
more [modules](#modules) and/or [lifecycle hooks](#lifecycle-hooks).
```js
// prettier-ignore
@@ -78,6 +80,12 @@ You can leverage one or both strategies. Which one(s) you choose depend on your
application's requirements. Each [module](#modules) defined by the extension
becomes available to the core application via the `ExtensionManager`.
#### Registering at Runtime
The `@ohif/viewer` uses a [configuration file](#) at startup. The schema for
that file includes an `Extensions` key that supports an array of extensions to
register.
```js
// prettier-ignore
const config = {
@@ -91,19 +99,13 @@ const config = {
}
```
#### Registering at Runtime
The `@ohif/viewer` uses a [configuration file](#) at startup. The schema for
that file includes an `Extensions` key that supports an array of extensions to
register.
#### Registering at Build Time
The `@ohif/viewer` works best when built as a "Progressive Web Application"
(PWA). If you know the extensions your application will need, you can specify
them at "build time" to leverage advantages afforded to us by modern tooling:
- Code Splitting
- Code Splitting (dynamic imports)
- Tree Shaking
- Dependency deduplication
@@ -141,6 +143,8 @@ differently.
| [Toolbar](./modules/toolbar.md) | Adds buttons or custom components to the toolbar | Toolbar button, nested buttons, custom |
| [Viewport](./modules/viewport.md) | Adds a component responsible for rendering a "DisplaySet" | `<CornerstoneViewport />`, `<DicomPdfViewport />` |
<figure style="text-align: center; font-style: italic;">Tbl. Module types with abridged descriptions and examples. Each module links to a dedicated documentation page.</figure>
### Contexts
The `@ohif/viewer` tracks "active contexts" that extensions can use to scope
@@ -153,9 +157,66 @@ An extension module can use these to say "Only show this Toolbar Button if the
active viewport is a Cornerstone viewport." This helps us use the appropriate UI
and behaviors depending on the current contexts.
For example, if we have hotkey that "rotates the active viewport", each Viewport
module that supports this behavior can add a command with the same name, scoped
to the appropriate context. When the `command` is fired, the "active contexts"
are used to determine the appropriate implementation of the rotate behavior.
## Consuming Extensions
...
We consume extensions, via the `ExtensionManager`, in our `@ohif/viewer`
project.
```js
const extensionManager = new ExtensionManager({
commandsManager,
servicesManager,
});
// prettier-ignore
extensionManager.registerExtensions([ /** **/ ]);
```
The `@ohif/viewer` project handles data fetching, basic routing, wires up UI
services, and is the home to the more bespoke application logic that doesn't
make as much sense to make reusable.
Long-term, replacing the `@ohif/viewer` application and consuming extensions
(and the `ExtensionManager`) in your own project is the ideal path for
applications requiring a high degree of customization that can't be achieved
with current theming, configuration, extension, and services support.
If you're not sure how to achieve your goals with the extensibility available
today, create a GitHub issue!
### `ExtensionManager`
The `ExtensionManager` is a class made available to us via the `@ohif/core`
project (platform/core). Our application instantiates a single instance of it,
and provides a `ServicesManager` and `CommandsManager` along with the application's
configuration through the appConfig key (optional).
```js
const commandsManager = new CommandsManager();
const servicesManager = new ServicesManager();
const extensionManager = new ExtensionManager({
commandsManager,
servicesManager,
appConfig
});
```
The `ExtensionManager` only has a few public members:
- `registerExtension` - Registers a single extension
- `registerExtensions` - Registers an array of extensions
- `modules` - An object containing registered extensions by `MODULE_TYPE`
During registration, lifecycle hooks and modules have access to the extension's config,
the application's config and `ExtensionManager`'s `ServicesManager` and `CommandsManager` instances.
Our `@ohif/viewer` uses the `modules` member to access registered extensions at
appropriate places in our application.
## Maintained Extensions
@@ -1,12 +1,31 @@
# Lifecylce Hook: preRegistration
If an extension defines the `preRegistration` lifecycle hook, it is called
before any modules are registered in the `ExtensionManager`.
before any modules are registered in the `ExtensionManager`. This hook can be
used to:
- initialize 3rd party libraries
- register event listeners
- add or call services
- add or call commands
The `preRegistration` hook receives an object containing the
`ExtensionManager`'s associated `ServicesManager`, `CommandsManager`, and any
`configuration` that was provided with the extension at time of registration.
_Example `preRegistration` hook implementation_
```js
export default {
id: 'MyExampleExtension',
/**
* @param {object} params
* @param {object} params.configuration
* @param {ServicesManager} params.servicesManager
* @param {CommandsManager} params.commandsManager
* @returns void
*/
preRegistration({ servicesManager, commandsManager, configuration }) {
console.log('Wiring up important stuff.');
+151 -27
View File
@@ -1,34 +1,158 @@
# Module: Commands
The Commands Module allows us to register one or more commands scoped to
specific contexts. Commands can be run by [hotkeys][#], [toolbar buttons][#],
and any registered custom react component (like a [viewport][#] or [panel][#]).
- [Overview](#overview)
- [Command Definitions](#command-definitions)
- [Commands Manager](#commands-manager)
- [Instantiating](#instatiating)
- [Public API](#public-api)
- [Contexts](#contexts)
## Overview
An extension can register a Commands Module by defining a `getCommandsModule`
method. The Commands Module allows us to register one or more commands scoped to
specific [contexts](./../index.md#contexts). Commands have several unique
characteristics that make them tremendously powerful:
- Multiple implementations for the same command can be defined
- Only the correct command's implementation will be run, dependent on the
application's "context"
- Commands can be called from extensions, modules, and the consuming application
Here is a simple example commands module:
```js
{
getCommandsModule() {
return {
actions: {
speak: ({ viewports, words }) => {
console.log(viewports, words);
},
},
definitions: {
rotateViewportCW: {
commandFn: actions.rotateViewport,
storeContexts: ['viewports'],
options: { rotation: 90 }
},
rotateViewportCCW: {
commandFn: actions.rotateViewport,
storeContexts: ['viewports'],
options: { rotation: -90 },
context: 'ACTIVE_VIEWER::CORNERSTONE'
},
},
defaultContext: 'VIEWER'
}
}
export default {
id: 'example-commands-module',
/**
* @param {object} params
* @param {ServicesManager} params.servicesManager
* @param {CommandsManager} params.commandsManager
*/
getCommandsModule({ servicesManager, commandsManager }) {
return {
definitions: {
sayHello: {
commandFn: ({ words }) => {
console.log(words);
},
options: { words: 'Hello!' },
},
},
defaultContext: 'VIEWER',
};
},
};
```
Each definition returned by the Commands Module is registered to the
`ExtensionManager`'s `CommandsManager`.
## Command Definitions
The command definition consists of a named command (`myCommandName` below) and a
`commandFn`. The command name is used to call the command, and the `commandFn`
is the "command" that is actioned.
```js
myCommandName: {
commandFn: ({ viewports, other, options }) => { },
storeContexts: ['viewports'],
options: { words: 'Just kidding! Goodbye!' },
context: 'ACTIVE_VIEWPORT::CORNERSTONE',
}
```
| Property | Type | Description |
| --------------- | ------------------ | --------------------------------------------------------------------------------------------------------------------------------------- |
| `commandFn` | func | The function to call when command is run. Receives `options` and `storeContexts`. |
| `storeContexts` | string[] | (optional) Expected state objects to be passed in as props. Located using `getAppState` fn defined at `CommandsManager`'s instatiation. |
| `options` | object | (optional) Arguments to pass at the time of calling to the `commandFn` |
| `context` | string[] or string | (optional) Overrides the `defaultContext`. Let's us know if command is currently "available" to be run. |
## Command Behavior
**I have many similar commands. How can I share their `commandFn` and make it
reusable?**
This is where `storeContexts` and `options` come in. We use these in our
`setToolActive` command. `storeContexts` helps us identify our `activeViewport`,
and `options` allow us to pass in the name of a tool we would like to set as
active.
**If there are multiple valid commands for the application's active contexts**
- What happens: all commands are run
- When to use: A `clearData` command that cleans up state for multiple
extensions
**If no commands are valid for the application's active contexts**
- What happens: a warning is printed to the console
- When to use: a `hotkey` (like "invert") that doesn't make sense for the
current viewport (PDF or HTML)
## `CommandsManager`
The `CommandsManager` is a class defined in the `@ohif/core` project. A single
instance of it should be defined in the consuming application, and it should be
used when constructing the `ExtensionManager`.
### Instantiating
When we instantiate the `CommandsManager`, we need to pass it two methods:
- `getAppState` - Should return the application's state when called
- `getActiveContexts` - Should return the application's active contexts when
called
These methods are used internally to help determine which commands are currently
valid, and how to provide them with any state they may need at the time they are
called.
```js
const commandsManager = new CommandsManager({
getAppState,
getActiveContexts,
});
```
### Public API
If you would like to run a command in the consuming app or an extension, you can
use one of the following methods:
```js
// Returns all commands for a given context
commandsManager.getContext('string');
// Attempts to run a command
commandsManager.runCommand('speak', { command: 'hello' });
// Run command, but override the active contexts
commandsManager.runCommand('speak', { command: 'hello' }, ['VIEWER']);
```
The `ExtensionManager` handles registering commands and creating contexts, so
most consumer's won't need these methods. If you find yourself using these, ask
yourself "why can't I register these commands via an extension?"
```js
// Used by the `ExtensionManager` to register new commands
commandsManager.registerCommand('context', 'name', commandDefinition);
// Creates a new context; clears the context if it already exists
commandsManager.createContext('string');
```
### Contexts
It is up to the consuming application to define what contexts are possible, and
which ones are currently active. As extensions depend heavily on these, we will
likely publish guidance around creating contexts, and ways to override extension
defined contexts in the near future. If you would like to discuss potential
changes to how contexts work, please don't hesistate to createa new GitHub
issue.
[Some additional information on Contexts can be found here.](./../index.md#contexts)
+27 -4
View File
@@ -1,6 +1,20 @@
# Module: Panel
...
An extension can register a Panel Module by defining a `getPanelModule` method.
The panel module provides the ability to define `menuOptions` and `components`
that can be used by the consuming application. `components` are React Components
that can be displayed in the consuming application's "Panel" Component.
![Panel Extension](../../assets/img/extensions-panel.gif)
<center><i>A panel extension example</i></center>
The `menuOptions`'s `target` key points to a registered `components`'s `id`. A
`defaultContext` is applied to all `menuOption`s; however, each `menuOption` can
optional provide it's own `context` value.
The `getPanelModule` receives an object containing the `ExtensionManager`'s
associated `ServicesManager` and `CommandsManager`.
```js
import MyComponent from './MyComponent.js';
@@ -10,15 +24,24 @@ export default {
/**
* @param {object} params
* @param {object} params.servicesManager
* @param {ServicesManager} params.servicesManager
* @param {CommandsManager} params.commandsManager
*/
getPanelModule({ servicesManager }) {
getPanelModule({ servicesManager, commandsManager }) {
return {
menuOptions: [
{
// A suggested icon
// Available icons determined by consuming app
icon: 'list',
// A suggested label
label: 'Magic',
// 'right' or 'left'
from: 'right',
// The target component to toggle open/close
target: 'target-component-id',
// Overrides `defaultContext`, if specified
context: ['ACTIVE_VIEWPORT:MAGIC'],
},
],
components: [
@@ -27,7 +50,7 @@ export default {
component: MyComponent,
},
],
defaultContext: ['VIEWER'],
defaultContext: ['ROUTE:VIEWER'],
};
},
};
@@ -1,6 +1,37 @@
# Module: SOP Class Handler
...
An extension can register a SOP Class Handler Module by defining a
`getSopClassHandlerModule` method. The SOP Class Handler is a bit different from
the other modules, as it doesn't provide a `1:1` schema for UI or provide it's
own components. It instead defines:
- `sopClassUids`: an array of string SOP Class UIDs that the
`getDisplaySetFromSeries` method should be applied to.
- `getDisplaySetFromSeries`: a method that maps series and study metadata to a
display set
A `displaySet` has the following shape:
```js
return {
plugin: 'html',
modality: 'SR',
displaySetInstanceUid: 0,
wadoRoot: study.getData().wadoRoot,
wadoUri: instance.getData().wadouri,
sopInstanceUid: instance.getSOPInstanceUID(),
seriesInstanceUid: series.getSeriesInstanceUID(),
studyInstanceUid: study.getStudyInstanceUID(),
authorizationHeaders,
};
```
Where the `plugin` key is used to influence the default `ViewportComponent` for
rendering the `displaySet`. Additional properties are passed to the
`ViewportComponent` and used by the default `StudyBrowser` to render
"thumbnails" for each `displaySet`
## Example SOP Class Handler Module
```js
const SOP_CLASS_UIDS = {
@@ -8,29 +39,52 @@ const SOP_CLASS_UIDS = {
ENHANCED_SR: '1.2.840.10008.5.1.4.1.1.88.22',
};
const sopClassHandlerModule = {
id: 'OHIFDicomHtmlSopClassHandler',
sopClassUids: Object.values(SOP_CLASS_UIDS),
export default {
id: 'example-sop-class-handler-module',
/**
* @param {object} series -
* @param {object} study -
* @param {object} dicomWebClient -
* @param {object} authorizationHeaders -
*/
getDisplaySetFromSeries(series, study, dicomWebClient, authorizationHeaders) {
const instance = series.getFirstInstance();
/**
* @param {object} params
* @param {ServicesManager} params.servicesManager
* @param {CommandsManager} params.commandsManager
*/
getSopClassHandlerModule({ servicesManager, commandsManager }) {
return {
id: 'OHIFDicomHtmlSopClassHandler',
sopClassUids: Object.values(SOP_CLASS_UIDS),
return {
plugin: 'html',
displaySetInstanceUid: 0,
wadoRoot: study.getData().wadoRoot,
wadoUri: instance.getData().wadouri,
sopInstanceUid: instance.getSOPInstanceUID(),
seriesInstanceUid: series.getSeriesInstanceUID(),
studyInstanceUid: study.getStudyInstanceUID(),
authorizationHeaders,
};
},
/**
* @param {object} series -
* @param {object} study -
* @param {object} dicomWebClient -
* @param {object} authorizationHeaders -
*/
getDisplaySetFromSeries(series, study, dicomWebClient, authorizationHeaders) {
const instance = series.getFirstInstance();
return {
plugin: 'html',
displaySetInstanceUid: 0,
wadoRoot: study.getData().wadoRoot,
wadoUri: instance.getData().wadouri,
sopInstanceUid: instance.getSOPInstanceUID(),
seriesInstanceUid: series.getSeriesInstanceUID(),
studyInstanceUid: study.getStudyInstanceUID(),
authorizationHeaders,
};
},
}
};
```
## `@ohif/viewer` usage
We use the `sopClassHandlerModule`s in three different places:
- `ViewerLocalFileData.js`
- `ViewerRetrieveStudyData.js`
- `StandaloneRouting.js`
Each time, it is used to map study and series data to `displaySets`. It does
this by working alongside the `StudyMetadataManager` in `@ohif/core`. That
manager has the method `createDisplaySets` that takes an array of
`sopClassHandlerModules`.
+116 -34
View File
@@ -1,48 +1,130 @@
# Module: Toolbar
An extension can register a Toolbar Module by providing a `getToolbarModule()`
method that returns a React Component. The component does not receive any props.
If you want to modify or react to state, you will need to connect to the redux
store. The given toolbar must determine its set of elements and the context of
them. The set of elements will be listed on toolbar `definitions`.
An extension can register a Toolbar Module by defining a `getToolbarModule`
method. This module is commonly used to define:
![Toolbar Extension](../assets/img/extensions-toolbar.gif)
- [Toolbar buttons](#button-definitions)
- [Nested toolbar menus](#nested-toolbar-menus)
- [Custom components](#custom-components)
<center><i>A toolbar extension example</i></center>
![Toolbar Extension](../../assets/img/extensions-toolbar.gif)
Toolbar components are rendered in the `ToolbarRow` component.
<center><i>Example toolbar button using the Dialog Service to show CINE controls.</i></center>
For a complete example implementation,
[check out the OHIFCornerstoneViewport's Toolbar Module](https://github.com/OHIF/Viewers/blob/master/extensions/cornerstone/src/toolbarModule.js).
## Example Toolbar Module
## Toolbar Custom Component
Toolbar elements can define its own custom react component to be consumed when
rendering it. So far, it accepts `Functional` and `Class` Components. For that,
you just need to expose your `CustomToolbarComponent` as the value of key
`CustomComponent`. In case the property `CustomComponent` is not present, a
default toolbar component will be used to render it. See bellow
The Toolbar Module should return an array of `definitions` and a
`defaultContext`. There are currently a few different variations of definitions,
each one is detailed further down.
```js
definitions: [
...
export default {
id: 'example-toolbar-module',
/**
* @param {object} params
* @param {ServicesManager} params.servicesManager
* @param {CommandsManager} params.commandsManager
*/
getToolbarModule({ servicesManager, commandsManager }) {
return {
definitions: [
/* Array of definitions */
],
defaultContext: ['ROUTE:VIEWER'],
};
},
};
```
## Button Definitions
The simplest definition has the following properties:
```js
{
id: 'StackScroll',
label: 'Stack Scroll',
icon: 'bars',
type: 'setToolActive',
commandName: 'setToolActive',
commandOptions: { toolName: 'StackScroll' },
},
```
| property | description | values |
| ---------------- | ----------------------------------------------------------------- | ----------------------------------------- |
| `id` | Unique string identifier for the definition | \* |
| `label` | User/display friendly to show in UI | \* |
| `icon` | A string name for an icon supported by the consuming application. | \* |
| `type` | Used to determine the button's component and behavior | `"setToolActive"`, `"command"` |
| `commandName` | (optional) The command to run when the button is used. | Any command registed by a `CommandModule` |
| `commandOptions` | (optional) Options to pass the target `commandName` | \* |
| `context` | (optional) Overrides module's `defaultContext` | Array of string context names |
Where a button with a `type` of `setToolActive` has an "active" styling applied
when clicked; removing the active styling from all other buttons.
## Nested Toolbar Menus
You can indicate that buttons should be grouped and nested in a submenu by
including `buttons` property in a definition:
```js
{
id: 'More',
label: 'More',
icon: 'ellipse-circle',
buttons: [
{
id: 'Custom',
label: 'Custom',
icon: 'custom-icon',
CustomComponent: CustomToolbarComponent,
}
...
]
id: 'cstInvert',
label: 'Invert',
icon: 'circle',
type: 'command',
commandName: 'invertViewport',
},
],
},
```
![Toolbar Extension](../../assets/img/extensions-toolbar-nested.gif)
<center><i>Example toolbar button demonstrating nested buttons.</i></center>
## Custom Components
The Toolbar Modules supports rendering custom components in place of the
application's default. In place of the `type`, `commandName`, and
`commandOptions` properties, we instead specify a `CustomComponent`.
```js
{
id: 'Custom',
label: 'Custom',
icon: 'custom-icon',
CustomComponent: CustomToolbarComponent,
}
```
`CustomComponent` components will receive the following props:
The `CustomComponent` components will receive the following props:
- parentContext: parent context. (In most of the cases it will be a ToolbarRow
instance)
- toolbarClickCallback: callback method when clicking on toolbar
- button: its own definition object
- key: react key prop
- activeButtons: list of active elements
- isActive: if current
```html
<CustomComponent
parentContext="{parentContext}"
toolbarClickCallback="{_handleToolbarButtonClick.bind(this)}"
button="{button}"
key="{button.id}"
activeButtons="{activeButtonsIds}"
isActive="{isActive}"
/>
```
| Property | Type | Description |
| ---------------------- | -------- | ------------------------------- |
| `activeButtons` | string[] | list of active buttons |
| `button` | object | its own definition object |
| `key` | string | React key prop |
| `isActive` | boolean | If current button is active |
| `parentContext` | ? | The parent component's context? |
| `toolbarClickCallback` | func | Callback method for clicks |
+32 -15
View File
@@ -1,19 +1,39 @@
# Module: Viewport
An extension can register a Viewport Module by providing a `getViewportModule()`
method that returns a React Component. The React component will receive the
following props:
An extension can register a Viewport Module by defining a `getViewportModule`
method that returns a React component. Currently, we use viewport components to
add support for:
```js
children: PropTypes.arrayOf(PropTypes.element)
studies: PropTypes.object,
displaySet: PropTypes.object,
viewportData: PropTypes.object, // { studies, displaySet }
viewportIndex: PropTypes.number,
children: PropTypes.node,
customProps: PropTypes.object
- 2D Medical Image Viewing (cornerstone ext.)
- Structured Reports as HTML (dicom html ext.)
- Encapsulated PDFs as PDFs (dicom pdf ext.)
- Whole Slide Microscopy Viewing (whole slide ext.)
- etc.
The general pattern is, the [`sopClassHandlerModule`](#) helps us determine
which Viewport Component a set of `sopClassUIDs` should default to. The Viewport
Component receives props containing a display set it should know how to render.
## Viewport Component Props
Each `ViewportComponent` will receive the following props:
```html
<viewportComponent
viewportData="{viewportData}"
viewportIndex="{viewportIndex}"
children="{[children]}"
/>
```
| Property | Type | Description |
| --------------- | --------------- | --------------------------------- |
| `children` | React.element[] | |
| `viewportData` | object | `viewportSpecificData` (probably) |
| `viewportIndex` | number | |
### `@ohif/viewer`
Viewport components are managed by the `ViewportGrid` Component. Which Viewport
component is used depends on:
@@ -23,7 +43,4 @@ component is used depends on:
![Cornerstone Viewport](../../assets/img/extensions-viewport.png)
<center><i>An example of three Viewports</i></center>
For a complete example implementation,
[check out the OHIFCornerstoneViewport](https://github.com/OHIF/Viewers/blob/master/extensions/cornerstone/src/OHIFCornerstoneViewport.js).
<center><i>An example of three cornerstone Viewports</i></center>
+1 -1
View File
@@ -13,7 +13,7 @@ like user preferences and previous query paramaters. Because of this, the Viewer
has been built to be highly configurable to work with almost any web accessible
data source.
![scope-of-project diagram](./assets/img/scope-of-project.png)
![scope-of-project diagram](./../assets/img/scope-of-project.png)
To be more specific, the OHIF Viewer is a collection of HTML, JS, and CSS files.
These can be delivered to your end users however you would like:
+1 -1
View File
@@ -43,7 +43,7 @@ translate to in practice?
```js
// In the application
const UINotificationService = createUINotificationService();
import UINotificationService from '@ohif/core';
const servicesManager = new ServicesManager();
servicesManager.registerService(UINotificationService);
+65 -1
View File
@@ -1,3 +1,67 @@
# Viewer: Configuration
...
We maintain a number of common viewer application configurations at
[`<root>/platform/viewer/public/configs`][config-dir]. How these values are
passed to the viewer depend on how it's deployed, but the two most common paths
are:
- `index.html` looks for `https://your-website.com/app-config.js` OR
- `index.html` passes the values to `OHIF.installViewer()`
```js
window.config = {
routerBasename: '/',
whiteLabelling: {},
extensions: [],
showStudyList: true,
filterQueryParam: false,
servers: {
dicomWeb: [
{
name: 'DCM4CHEE',
wadoUriRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/wado',
qidoRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/rs',
wadoRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/rs',
qidoSupportsIncludeField: true,
imageRendering: 'wadors',
thumbnailRendering: 'wadors',
enableStudyLazyLoad: true,
},
],
},
// Supported Keys: https://craig.is/killing/mice
hotkeys: [
{ commandName: 'rotateViewportCW', label: 'Rotate Right', keys: ['r'] },
{ commandName: 'rotateViewportCCW', label: 'Rotate Left', keys: ['l'] },
{ commandName: 'invertViewport', label: 'Invert', keys: ['i'] },
{
commandName: 'flipViewportVertical',
label: 'Flip Horizontally',
keys: ['h'],
},
{
commandName: 'flipViewportHorizontal',
label: 'Flip Vertically',
keys: ['v'],
},
],
/* Configuration passed to the bundled cornerstone extension
*
* The cornerstone extension is currently tightly coupled to the platform.
* Until we're able to decouple it, this key will serve as a workaround to
* pass it configuration.
*/
cornerstoneExtensionConfig: {
/* Whether to show/hide annotation "handles" */
hideHandles: true,
},
};
```
<!--
LINKS
-->
<!-- prettier-ignore-start -->
[config-dir]: https://github.com/OHIF/Viewers/tree/master/platform/viewer/public/config
<!-- prettier-ignore-end -->
@@ -0,0 +1,23 @@
# Environment Variables
There are a number of environment variables we use at build time to influence the output application's behavior.
```bash
# Application
NODE_ENV=< production | development >
DEBUG=< true | false >
APP_CONFIG=< relative path to application configuration file >
PUBLIC_URL=<>
VERSION_NUMBER=<Set by CircleCI>
BUILD_NUM=<Set by CircleCI>
# i18n
USE_LOCIZE=<false>
LOCIZE_PROJECTID=<ProjectID to pull translations for>
LOCIZE_API_KEY=<To enable Locize live editing of translations>
```
## Setting Environment Variables
- `npx cross-env`
- `.env` files
- env variables on build machine, or for terminal session
+4 -2
View File
@@ -12,8 +12,10 @@ export default {
*/
preRegistration({
servicesManager,
configuration: extensionConfiguration,
servicesManager = {},
commandsManager = {},
appConfig = {},
configuration = {},
}) {},
/**
+74
View File
@@ -3,6 +3,80 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [2.2.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.2.0...@ohif/extension-cornerstone@2.2.1) (2019-12-20)
### Bug Fixes
* 🐛 1241: Make Plugin switch part of ToolbarModule ([#1322](https://github.com/OHIF/Viewers/issues/1322)) ([6540e36](https://github.com/OHIF/Viewers/commit/6540e36818944ac2eccc696186366ae495b33a04)), closes [#1241](https://github.com/OHIF/Viewers/issues/1241)
# [2.2.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.1.1...@ohif/extension-cornerstone@2.2.0) (2019-12-20)
### Features
* 🎸 Configuration so viewer tools can nix handles ([#1304](https://github.com/OHIF/Viewers/issues/1304)) ([63594d3](https://github.com/OHIF/Viewers/commit/63594d36b0bdba59f0901095aed70b75fb05172d)), closes [#1223](https://github.com/OHIF/Viewers/issues/1223)
## [2.1.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.1.0...@ohif/extension-cornerstone@2.1.1) (2019-12-16)
### Bug Fixes
* 🐛 add WwwcRegionTool to cornerstone tools initialization ([#1302](https://github.com/OHIF/Viewers/issues/1302)) ([d5bf728](https://github.com/OHIF/Viewers/commit/d5bf72851a32dff9fd3fc09332ea5250bc7e6114))
# [2.1.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.0.2...@ohif/extension-cornerstone@2.1.0) (2019-12-11)
### Features
* 🎸 DICOM SR STOW on MeasurementAPI ([#954](https://github.com/OHIF/Viewers/issues/954)) ([ebe1af8](https://github.com/OHIF/Viewers/commit/ebe1af8d4f75d2483eba869655906d7829bd9666)), closes [#758](https://github.com/OHIF/Viewers/issues/758)
## [2.0.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.0.1...@ohif/extension-cornerstone@2.0.2) (2019-12-11)
**Note:** Version bump only for package @ohif/extension-cornerstone
## [2.0.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.0.0...@ohif/extension-cornerstone@2.0.1) (2019-12-09)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [2.0.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@1.7.2...@ohif/extension-cornerstone@2.0.0) (2019-12-09)
* feat!: Ability to configure cornerstone tools via extension configuration (#1229) ([55a5806](https://github.com/OHIF/Viewers/commit/55a580659ecb74ca6433461d8f9a05c2a2b69533)), closes [#1229](https://github.com/OHIF/Viewers/issues/1229)
### BREAKING CHANGES
* modifies the exposed react <App /> components props. The contract for providing configuration for the app has changed. Please reference updated documentation for guidance.
## [1.7.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@1.7.1...@ohif/extension-cornerstone@1.7.2) (2019-12-02)
**Note:** Version bump only for package @ohif/extension-cornerstone
+32
View File
@@ -67,6 +67,38 @@ Our Viewport wraps [cornerstonejs/react-cornerstone-viewport][react-viewport]
and is connected the redux store. This module is the most prone to change as we
hammer out our Viewport interface.
## Tool Configuration
Tools can be configured through extension configuration using the tools key:
```js
...
cornerstoneExtensionConfig: {
tools: {
ArrowAnnotate: {
configuration: {
getTextCallback: (callback, eventDetails) => callback(prompt('Enter your custom annotation')),
},
},
},
},
...
```
## Annotate Tools Configuration
*We currently support one property for annotation tools.*
### Hide handles
This extension configuration allows you to toggle on/off handle rendering for all annotate tools:
```js
...
cornerstoneExtensionConfig: {
hideHandles: true,
},
...
## Resources
### Repositories
+4 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/extension-cornerstone",
"version": "1.7.2",
"version": "2.2.1",
"description": "OHIF extension for Cornerstone",
"author": "OHIF",
"license": "MIT",
@@ -32,9 +32,9 @@
"@ohif/ui": "^0.50.0",
"cornerstone-core": "^2.2.8",
"cornerstone-math": "^0.1.8",
"cornerstone-tools": "^4.8.0",
"cornerstone-tools": "^4.9.0",
"cornerstone-wado-image-loader": "^3.0.0",
"dcmjs": "^0.6.1",
"dcmjs": "^0.8.2",
"dicom-parser": "^1.8.3",
"hammerjs": "^2.0.8",
"prop-types": "^15.6.2",
@@ -47,6 +47,7 @@
"dependencies": {
"@babel/runtime": "^7.5.5",
"classnames": "^2.2.6",
"lodash.merge": "^4.6.2",
"lodash.throttle": "^4.1.1",
"query-string": "^6.8.3",
"react-cornerstone-viewport": "2.x.x"
@@ -248,10 +248,13 @@ class OHIFCornerstoneViewport extends Component {
// TODO: Does it make more sense to use Context?
if (this.props.children && this.props.children.length) {
childrenWithProps = this.props.children.map((child, index) => {
return React.cloneElement(child, {
viewportIndex: this.props.viewportIndex,
key: index,
});
return (
child &&
React.cloneElement(child, {
viewportIndex: this.props.viewportIndex,
key: index,
})
);
});
}
+117 -7
View File
@@ -2,6 +2,7 @@ import cornerstone from 'cornerstone-core';
import cornerstoneTools from 'cornerstone-tools';
import OHIF from '@ohif/core';
import setCornerstoneLayout from './utils/setCornerstoneLayout.js';
import { getEnabledElement } from './state';
import CornerstoneViewportDownloadForm from './CornerstoneViewportDownloadForm';
const scroll = cornerstoneTools.import('util/scroll');
@@ -148,18 +149,121 @@ const commandsModule = ({ servicesManager }) => {
showDownloadViewportModal: ({ title, viewports }) => {
const activeViewportIndex = viewports.activeViewportIndex;
const { UIModalService } = servicesManager.services;
UIModalService.show({
content: CornerstoneViewportDownloadForm,
title,
contentProps: {
activeViewportIndex,
onClose: UIModalService.hide,
},
if (UIModalService) {
UIModalService.show({
content: CornerstoneViewportDownloadForm,
title,
contentProps: {
activeViewportIndex,
onClose: UIModalService.hide,
},
});
}
},
updateTableWithNewMeasurementData({
toolType,
measurementNumber,
location,
description,
}) {
// Update all measurements by measurement number
const measurementApi = OHIF.measurements.MeasurementApi.Instance;
const measurements = measurementApi.tools[toolType].filter(
m => m.measurementNumber === measurementNumber
);
measurements.forEach(measurement => {
measurement.location = location;
measurement.description = description;
measurementApi.updateMeasurement(measurement.toolType, measurement);
});
measurementApi.syncMeasurementsAndToolData();
// Update images in all active viewports
cornerstone.getEnabledElements().forEach(enabledElement => {
cornerstone.updateImage(enabledElement.element);
});
},
getNearbyToolData({ element, canvasCoordinates, availableToolTypes }) {
const nearbyTool = {};
let pointNearTool = false;
availableToolTypes.forEach(toolType => {
const elementToolData = cornerstoneTools.getToolState(
element,
toolType
);
if (!elementToolData) {
return;
}
elementToolData.data.forEach((toolData, index) => {
let elementToolInstance = cornerstoneTools.getToolForElement(
element,
toolType
);
if (!elementToolInstance) {
elementToolInstance = cornerstoneTools.getToolForElement(
element,
`${toolType}Tool`
);
}
if (!elementToolInstance) {
console.warn('Tool not found.');
return undefined;
}
if (
elementToolInstance.pointNearTool(
element,
toolData,
canvasCoordinates
)
) {
pointNearTool = true;
nearbyTool.tool = toolData;
nearbyTool.index = index;
nearbyTool.toolType = toolType;
}
});
if (pointNearTool) {
return false;
}
});
return pointNearTool ? nearbyTool : undefined;
},
removeToolState: ({ element, toolType, tool }) => {
cornerstoneTools.removeToolState(element, toolType, tool);
cornerstone.updateImage(element);
},
setCornerstoneLayout: () => {
setCornerstoneLayout();
}
};
const definitions = {
getNearbyToolData: {
commandFn: actions.getNearbyToolData,
storeContexts: [],
options: {},
},
removeToolState: {
commandFn: actions.removeToolState,
storeContexts: [],
options: {},
},
updateTableWithNewMeasurementData: {
commandFn: actions.updateTableWithNewMeasurementData,
storeContexts: [],
options: {},
},
showDownloadViewportModal: {
commandFn: actions.showDownloadViewportModal,
storeContexts: ['viewports'],
@@ -248,6 +352,12 @@ const commandsModule = ({ servicesManager }) => {
storeContexts: [],
options: {},
},
setCornerstoneLayout: {
commandFn: actions.setCornerstoneLayout,
storeContexts: [],
options: {},
context: 'VIEWER',
},
};
return {
+118 -74
View File
@@ -1,9 +1,10 @@
import OHIF from '@ohif/core';
import { SimpleDialog } from '@ohif/ui';
import cornerstone from 'cornerstone-core';
import csTools from 'cornerstone-tools';
import initCornerstoneTools from './initCornerstoneTools.js';
import merge from 'lodash.merge';
import queryString from 'query-string';
import { SimpleDialog } from '@ohif/ui';
import initCornerstoneTools from './initCornerstoneTools.js';
function fallbackMetaDataProvider(type, imageId) {
if (!imageId.includes('wado?requestType=WADO')) {
@@ -26,30 +27,33 @@ cornerstone.metaData.addProvider(fallbackMetaDataProvider, -1);
/**
*
* @param {object} configuration
* @param {Object} servicesManager
* @param {Object} configuration
* @param {Object|Array} configuration.csToolsConfig
*/
export default function init({ servicesManager, configuration = {} }) {
const { UIDialogService } = servicesManager.services;
export default function init({ servicesManager, configuration }) {
const callInputDialog = (data, event, callback) => {
let dialogId = UIDialogService.create({
content: SimpleDialog.InputDialog,
defaultPosition: {
x: (event && event.currentPoints.canvas.x) || 0,
y: (event && event.currentPoints.canvas.y) || 0,
},
showOverlay: true,
contentProps: {
title: 'Enter your annotation',
label: 'New label',
measurementData: data ? { description: data.text } : {},
onClose: () => UIDialogService.dismiss({ id: dialogId }),
onSubmit: value => {
callback(value);
UIDialogService.dismiss({ id: dialogId });
const { UIDialogService } = servicesManager.services;
if (UIDialogService) {
let dialogId = UIDialogService.create({
centralize: true,
isDraggable: false,
content: SimpleDialog.InputDialog,
useLastPosition: false,
showOverlay: true,
contentProps: {
title: 'Enter your annotation',
label: 'New label',
measurementData: data ? { description: data.text } : {},
onClose: () => UIDialogService.dismiss({ id: dialogId }),
onSubmit: value => {
callback(value);
UIDialogService.dismiss({ id: dialogId });
},
},
},
});
});
}
};
const { csToolsConfig } = configuration;
@@ -72,61 +76,101 @@ export default function init({ servicesManager, configuration = {} }) {
initCornerstoneTools(defaultCsToolsConfig);
// ~~ Toooools 🙌
const {
PanTool,
ZoomTool,
WwwcTool,
MagnifyTool,
StackScrollTool,
StackScrollMouseWheelTool,
// Touch
PanMultiTouchTool,
ZoomTouchPinchTool,
// Annotations
EraserTool,
BidirectionalTool,
LengthTool,
AngleTool,
FreehandRoiTool,
EllipticalRoiTool,
DragProbeTool,
RectangleRoiTool,
// Segmentation
BrushTool,
} = csTools;
const tools = [
PanTool,
ZoomTool,
WwwcTool,
MagnifyTool,
StackScrollTool,
StackScrollMouseWheelTool,
// Touch
PanMultiTouchTool,
ZoomTouchPinchTool,
// Annotations
EraserTool,
BidirectionalTool,
LengthTool,
AngleTool,
FreehandRoiTool,
EllipticalRoiTool,
DragProbeTool,
RectangleRoiTool,
// Segmentation
BrushTool,
];
const toolsGroupedByType = {
touch: [csTools.PanMultiTouchTool, csTools.ZoomTouchPinchTool],
annotations: [
csTools.ArrowAnnotateTool,
csTools.EraserTool,
csTools.BidirectionalTool,
csTools.LengthTool,
csTools.AngleTool,
csTools.FreehandRoiTool,
csTools.EllipticalRoiTool,
csTools.DragProbeTool,
csTools.RectangleRoiTool,
],
segmentation: [csTools.BrushTool],
other: [
csTools.PanTool,
csTools.ZoomTool,
csTools.WwwcTool,
csTools.WwwcRegionTool,
csTools.MagnifyTool,
csTools.StackScrollTool,
csTools.StackScrollMouseWheelTool,
],
};
tools.forEach(tool => csTools.addTool(tool));
let tools = [];
Object.keys(toolsGroupedByType).forEach(toolsGroup =>
tools.push(...toolsGroupedByType[toolsGroup])
);
csTools.addTool(csTools.ArrowAnnotateTool, {
configuration: {
getTextCallback: (callback, eventDetails) =>
callInputDialog(null, eventDetails, callback),
changeTextCallback: (data, eventDetails, callback) =>
callInputDialog(data, eventDetails, callback),
/* Add extension tools configuration here. */
const internalToolsConfig = {
ArrowAnnotate: {
configuration: {
getTextCallback: (callback, eventDetails) =>
callInputDialog(null, eventDetails, callback),
changeTextCallback: (data, eventDetails, callback) =>
callInputDialog(data, eventDetails, callback),
},
},
};
/* Abstract tools configuration using extension configuration. */
const parseToolProps = (props, tool) => {
const { annotations } = toolsGroupedByType;
// An alternative approach would be to remove the `drawHandlesOnHover` config
// from the supported configuration properties in `cornerstone-tools`
const toolsWithHideableHandles = annotations.filter(
tool => !['RectangleRoiTool', 'EllipticalRoiTool'].includes(tool.name)
);
let parsedProps = { ...props };
/**
* drawHandles - Never/Always show handles
* drawHandlesOnHover - Only show handles on handle hover (pointNearHandle)
*
* Does not apply to tools where handles aren't placed in predictable
* locations.
*/
if (
configuration.hideHandles !== false &&
toolsWithHideableHandles.includes(tool)
) {
if (props.configuration) {
parsedProps.configuration.drawHandlesOnHover = true;
} else {
parsedProps.configuration = { drawHandlesOnHover: true };
}
}
return parsedProps;
};
/* Add tools with its custom props through extension configuration. */
tools.forEach(tool => {
const toolName = tool.name.replace('Tool', '');
const externalToolsConfig = configuration.tools || {};
const externalToolProps = externalToolsConfig[toolName] || {};
const internalToolProps = internalToolsConfig[toolName] || {};
const props = merge(
internalToolProps,
parseToolProps(externalToolProps, tool)
);
csTools.addTool(tool, props);
});
// TODO -> We need a better way to do this with maybe global tool state setting all tools passive.
const BaseAnnotationTool = csTools.importInternal('base/BaseAnnotationTool');
tools.forEach(tool => {
if (tool.prototype instanceof BaseAnnotationTool) {
// BaseAnnotationTool would likely come from csTools lib exports
const toolName = new tool().name;
csTools.setToolPassive(toolName); // there may be a better place to determine name; may not be on uninstantiated class
}
});
csTools.setToolActive('Pan', { mouseButtonMask: 4 });
@@ -237,6 +237,15 @@ const definitions = [
},
],
},
{
id: 'Exit2DMPR',
label: 'Exit 2D MPR',
icon: 'times',
//
type: TOOLBAR_BUTTON_TYPES.COMMAND,
commandName: 'setCornerstoneLayout',
context: 'ACTIVE_VIEWPORT::VTK',
}
];
export default {
@@ -0,0 +1,22 @@
import { redux } from '@ohif/core';
const { setLayout } = redux.actions;
/**
* Update the current layout with a simple Cornerstone one
*
* @return void
*/
const setCornerstoneLayout = () => {
const layout = {
numRows: 1,
numColumns: 1,
viewports: [{ plugin: 'cornerstone' }],
};
const action = setLayout(layout);
window.store.dispatch(action);
}
export default setCornerstoneLayout;
+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.
# [1.1.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-html@1.0.2...@ohif/extension-dicom-html@1.1.0) (2019-12-11)
### Features
* 🎸 DICOM SR STOW on MeasurementAPI ([#954](https://github.com/OHIF/Viewers/issues/954)) ([ebe1af8](https://github.com/OHIF/Viewers/commit/ebe1af8d4f75d2483eba869655906d7829bd9666)), closes [#758](https://github.com/OHIF/Viewers/issues/758)
## [1.0.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-html@1.0.1...@ohif/extension-dicom-html@1.0.2) (2019-12-02)
**Note:** Version bump only for package @ohif/extension-dicom-html
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/extension-dicom-html",
"version": "1.0.2",
"version": "1.1.0",
"description": "OHIF extension for rendering structured reports to HTML",
"author": "OHIF",
"license": "MIT",
@@ -29,7 +29,7 @@
},
"peerDependencies": {
"@ohif/core": "^0.50.0",
"dcmjs": "^0.6.1",
"dcmjs": "^0.8.2",
"prop-types": "^15.6.2",
"react": "^16.8.6",
"react-dom": "^16.8.6"
+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.
## [1.0.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-pdf@1.0.0...@ohif/extension-dicom-pdf@1.0.1) (2019-12-16)
### Bug Fixes
* 🐛 Update ohif pdf extension to optionally use pdfjs ([#1162](https://github.com/OHIF/Viewers/issues/1162)) ([31e542d](https://github.com/OHIF/Viewers/commit/31e542dfd973c3e982ede6d02d79bfe65175e792)), closes [#1049](https://github.com/OHIF/Viewers/issues/1049)
# [1.0.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-pdf@0.50.4...@ohif/extension-dicom-pdf@1.0.0) (2019-09-27)
+3 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/extension-dicom-pdf",
"version": "1.0.0",
"version": "1.0.1",
"description": "OHIF extension for Dicom PDF",
"author": "OHIF",
"license": "MIT",
@@ -37,7 +37,8 @@
"dependencies": {
"@babel/runtime": "^7.5.5",
"classnames": "^2.2.6",
"lodash.isequal": "^4.5.0"
"lodash.isequal": "^4.5.0",
"pdfjs-dist": "^2.2.228"
},
"gitHead": "a5baa9228c0eda0df880136bde4420d78e6f8706"
}
@@ -0,0 +1,27 @@
import OHIF from '@ohif/core';
import { connect } from 'react-redux';
import OHIFDicomPDFViewport from './OHIFDicomPDFViewport';
const { setViewportActive } = OHIF.redux.actions;
const mapStateToProps = (state, ownProps) => {
const { activeViewportIndex } = state.viewports;
return { activeViewportIndex };
};
const mapDispatchToProps = (dispatch, ownProps) => {
const { viewportIndex } = ownProps;
return {
setViewportActive: () => {
dispatch(setViewportActive(viewportIndex));
},
};
};
const ConnectedOHIFDicomPDFViewer = connect(
mapStateToProps,
mapDispatchToProps
)(OHIFDicomPDFViewport);
export default ConnectedOHIFDicomPDFViewer;
@@ -0,0 +1,62 @@
.DicomPDFViewport {
--header-height: 50px;
}
.DicomPDFViewport #toolbar {
display: flex;
align-items: center;
color: #fff;
padding: 0.5em;
border-bottom: var(--ui-border-thickness) solid var(--ui-border-color);
height: var(--header-height);
}
.DicomPDFViewport #canvas {
height: calc(100% - var(--header-height));
overflow-y: scroll;
}
.DicomPDFViewport #text-layer {
height: 100%;
margin-top: var(--header-height);
}
.DicomPDFViewport #pdf-canvas-container {
position: relative;
}
.DicomPDFViewport canvas, #text-layer {
display: block;
margin: 0 auto;
overflow: hidden;
}
.DicomPDFViewport #toolbar button {
color: currentColor;
background-color: transparent;
font: inherit;
border: var(--ui-border-thickness) solid var(--ui-border-color);
border-radius: 3px;
padding: 0.25em 0.5em;
margin-right: 0.5em;
cursor: pointer;
}
.DicomPDFViewport #text-layer {
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
overflow: hidden;
opacity: 0.2;
line-height: 1;
}
.DicomPDFViewport #text-layer>span {
color: transparent;
position: absolute;
white-space: pre;
cursor: text;
transform-origin: 0% 0%;
}
+197 -23
View File
@@ -1,6 +1,14 @@
import React, { Component } from 'react';
import React, { Component, createRef } from 'react';
import dicomParser from 'dicom-parser';
import PDFJS from 'pdfjs-dist';
import PropTypes from 'prop-types';
import TypedArrayProp from './TypedArrayProp';
import './DicomPDFViewport.css';
import pdfjsBuild from 'pdfjs-dist/build/pdf';
import pdfjsWorker from 'pdfjs-dist/build/pdf.worker.entry';
pdfjsBuild.GlobalWorkerOptions.workerSrc = pdfjsWorker;
// TODO: Should probably use dcmjs for this
const SOP_CLASS_UIDS = {
@@ -8,16 +16,88 @@ const SOP_CLASS_UIDS = {
};
class DicomPDFViewport extends Component {
state = {
fileURL: null,
error: null,
};
constructor(props) {
super(props);
this.state = {
fileURL: null,
error: null,
currentPageIndex: 1,
pdf: null,
scale: 1,
};
this.canvas = createRef();
this.textLayer = createRef();
}
static propTypes = {
byteArray: TypedArrayProp.uint8,
useNative: PropTypes.bool,
viewportData: PropTypes.object,
activeViewportIndex: PropTypes.number,
setViewportActive: PropTypes.func,
viewportIndex: PropTypes.number,
};
renderPDF = (dataSet, byteArray) => {
static defaultProps = {
useNative: false,
};
async componentDidMount() {
const dataSet = this.parseByteArray(this.props.byteArray);
const fileURL = this.getPDFFileUrl(dataSet, this.props.byteArray);
this.setState(state => ({ ...state, fileURL }));
if (!this.props.useNative) {
const pdf = await PDFJS.getDocument(fileURL).promise;
this.setState(state => ({ ...state, pdf }), () => this.updatePDFCanvas());
}
}
updatePDFCanvas = async () => {
const { pdf, scale, currentPageIndex } = this.state;
const context = this.canvas.getContext('2d');
const page = await pdf.getPage(currentPageIndex);
let viewport = page.getViewport({ scale });
this.canvas.height = viewport.height;
this.canvas.width = viewport.width;
const renderContext = {
canvasContext: context,
viewport: viewport,
};
await page.render(renderContext);
const textContent = await page.getTextContent();
this.textLayer.innerHTML = '';
this.textLayer.style.height = viewport.height + 'px';
this.textLayer.style.width = viewport.width + 'px';
PDFJS.renderTextLayer({
textContent,
container: this.textLayer,
viewport,
textDivs: [],
});
};
componentDidUpdate(prevProps, prevState) {
const { currentPageIndex, scale } = this.state;
const newValidScale = prevState.scale !== scale && scale > 0;
const newValidPageNumber =
prevState.currentPageIndex !== currentPageIndex && currentPageIndex > 0;
if (newValidScale || newValidPageNumber) {
this.updatePDFCanvas();
}
}
getPDFFileUrl = (dataSet, byteArray) => {
let pdfByteArray = byteArray;
if (dataSet) {
@@ -36,50 +116,144 @@ class DicomPDFViewport extends Component {
const PDF = new Blob([pdfByteArray], { type: 'application/pdf' });
const fileURL = URL.createObjectURL(PDF);
this.setState({
fileURL,
});
return fileURL;
};
onPageChange = async event => {
const { currentPageIndex, pdf } = this.state;
let newPageIndex = currentPageIndex;
const action = event.target.getAttribute('data-pager');
if (action === 'prev') {
if (currentPageIndex === 1) {
return;
}
newPageIndex -= 1;
if (currentPageIndex < 0) {
newPageIndex = 0;
}
}
if (action === 'next') {
if (currentPageIndex === pdf.numPages - 1) {
return;
}
newPageIndex += 1;
if (currentPageIndex > pdf.numPages - 1) {
newPageIndex = pdf.numPages - 1;
}
}
this.setState(state => ({ ...state, currentPageIndex: newPageIndex }));
};
onZoomChange = () => {
let newZoomValue = this.state.scale;
const action = event.target.getAttribute('data-pager');
if (action === '+') {
newZoomValue += 0.25;
}
if (action === '-') {
newZoomValue -= 0.25;
}
this.setState(state => ({ ...state, scale: newZoomValue }));
};
parseByteArray = byteArray => {
const options = {
untilTag: '',
};
const options = { untilTag: '' };
let dataSet;
try {
dataSet = dicomParser.parseDicom(byteArray, options);
} catch (error) {
this.setState({
error,
});
this.setState(state => ({ ...state, error }));
}
return dataSet;
};
componentDidMount() {
const dataSet = this.parseByteArray(this.props.byteArray);
setViewportActiveHandler = () => {
const {
setViewportActive,
viewportIndex,
activeViewportIndex,
} = this.props;
this.renderPDF(dataSet, this.props.byteArray);
}
if (viewportIndex !== activeViewportIndex) {
setViewportActive(viewportIndex);
}
};
downloadPDFCanvas = () => {
const { fileURL } = this.state;
const a = document.createElement('a');
a.href = fileURL;
a.download = fileURL.substr(fileURL.lastIndexOf('/') + 1);
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
};
render() {
const { fileURL, pdf, error } = this.state;
return (
<div
className={'DicomPDFViewport'}
onClick={this.setViewportActiveHandler}
onScroll={this.setViewportActiveHandler}
style={{ width: '100%', height: '100%' }}
>
{this.state.fileURL && (
{!this.props.useNative ? (
<>
<div id="toolbar">
<div id="pager">
{pdf && pdf.numPages > 1 && (
<>
<button data-pager="prev" onClick={this.onPageChange}>
{`<`}
</button>
<button data-pager="next" onClick={this.onPageChange}>
{`>`}
</button>
</>
)}
<button data-pager="-" onClick={this.onZoomChange}>
{`-`}
</button>
<button data-pager="+" onClick={this.onZoomChange}>
{`+`}
</button>
<button onClick={this.downloadPDFCanvas}>Download</button>
</div>
</div>
<div id="canvas">
<div id="pdf-canvas-container">
<canvas
id="pdf-canvas"
ref={canvas => (this.canvas = canvas)}
/>
<div
id="text-layer"
ref={textLayer => (this.textLayer = textLayer)}
></div>
</div>
</div>
</>
) : (
<object
data={this.state.fileURL}
aria-label="PDF Viewer"
data={fileURL}
type="application/pdf"
width="100%"
height="100%"
/>
)}
{this.state.error && <h2>{JSON.stringify(this.state.error)}</h2>}
{error && <h2>{JSON.stringify(error)}</h2>}
</div>
);
}
@@ -11,6 +11,9 @@ class OHIFDicomPDFViewport extends Component {
studies: PropTypes.object,
displaySet: PropTypes.object,
viewportIndex: PropTypes.number,
viewportData: PropTypes.object,
activeViewportIndex: PropTypes.number,
setViewportActive: PropTypes.func,
};
state = {
@@ -30,42 +33,36 @@ class OHIFDicomPDFViewport extends Component {
componentDidMount() {
const { displaySet, studies } = this.props.viewportData;
const {
studyInstanceUid,
seriesInstanceUid,
sopInstanceUid,
wadoRoot,
wadoUri,
authorizationHeaders,
} = displaySet;
DicomLoaderService.findDicomDataPromise(displaySet, studies).then(
data => {
const byteArray = new Uint8Array(data);
this.setState({
byteArray: byteArray,
});
},
data => this.setState({ byteArray: new Uint8Array(data) }),
error => {
this.setState({
error,
});
this.setState({ error });
throw new Error(error);
}
);
}
render() {
const {
setViewportActive,
viewportIndex,
activeViewportIndex,
} = this.props;
const { byteArray, error } = this.state;
const { id, init, destroy } = OHIFDicomPDFViewport;
const pluginProps = { id, init, destroy };
return (
<OHIFComponentPlugin {...pluginProps}>
{this.state.byteArray && (
<DicomPDFViewport byteArray={this.state.byteArray} />
{byteArray && (
<DicomPDFViewport
byteArray={byteArray}
setViewportActive={setViewportActive}
viewportIndex={viewportIndex}
activeViewportIndex={activeViewportIndex}
/>
)}
{this.state.error && <h2>{JSON.stringify(this.state.error)}</h2>}
{error && <h2>{JSON.stringify(error)}</h2>}
</OHIFComponentPlugin>
);
}
@@ -0,0 +1,37 @@
/**
* We use this component to leverage "Code Splitting"
*
* Link: https://serverless-stack.com/chapters/code-splitting-in-create-react-app.html
*/
import React, { Component } from 'react';
export default function asyncComponent(importComponent) {
class AsyncComponent extends Component {
constructor(props) {
super(props);
this.state = {
component: null,
};
}
async componentDidMount() {
// Add dynamically loaded component to state
const { default: component } = await importComponent();
this.setState({
component: component,
});
}
render() {
const C = this.state.component;
// Render the loaded component, or null
return C ? <C {...this.props} /> : null;
}
}
return AsyncComponent;
}
+9 -4
View File
@@ -1,16 +1,21 @@
import asyncComponent from './asyncComponent.js';
import OHIFDicomPDFSopClassHandler from './OHIFDicomPDFSopClassHandler.js';
import OHIFDicomPDFViewport from './OHIFDicomPDFViewport.js';
const ConnectedOHIFDicomPDFViewer = asyncComponent(() =>
import(
/* webpackChunkName: "ConnectedOHIFDicomPDFViewer" */ './ConnectedOHIFDicomPDFViewer'
)
);
export default {
/**
* Only required property. Should be a unique value across all extensions.
*/
id: 'pdf',
getViewportModule() {
return OHIFDicomPDFViewport;
return ConnectedOHIFDicomPDFViewer;
},
getSopClassHandlerModule() {
return OHIFDicomPDFSopClassHandler;
}
},
};
+143
View File
@@ -3,6 +3,149 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.2.4](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.2.3...@ohif/extension-vtk@1.2.4) (2019-12-30)
**Note:** Version bump only for package @ohif/extension-vtk
## [1.2.3](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.2.2...@ohif/extension-vtk@1.2.3) (2019-12-20)
### Bug Fixes
* 🐛 1241: Make Plugin switch part of ToolbarModule ([#1322](https://github.com/OHIF/Viewers/issues/1322)) ([6540e36](https://github.com/OHIF/Viewers/commit/6540e36818944ac2eccc696186366ae495b33a04)), closes [#1241](https://github.com/OHIF/Viewers/issues/1241)
## [1.2.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.2.1...@ohif/extension-vtk@1.2.2) (2019-12-20)
**Note:** Version bump only for package @ohif/extension-vtk
## [1.2.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.2.0...@ohif/extension-vtk@1.2.1) (2019-12-20)
**Note:** Version bump only for package @ohif/extension-vtk
# [1.2.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.1.7...@ohif/extension-vtk@1.2.0) (2019-12-20)
### Features
* 🎸 Configuration so viewer tools can nix handles ([#1304](https://github.com/OHIF/Viewers/issues/1304)) ([63594d3](https://github.com/OHIF/Viewers/commit/63594d36b0bdba59f0901095aed70b75fb05172d)), closes [#1223](https://github.com/OHIF/Viewers/issues/1223)
## [1.1.7](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.1.6...@ohif/extension-vtk@1.1.7) (2019-12-19)
**Note:** Version bump only for package @ohif/extension-vtk
## [1.1.6](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.1.5...@ohif/extension-vtk@1.1.6) (2019-12-18)
**Note:** Version bump only for package @ohif/extension-vtk
## [1.1.5](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.1.4...@ohif/extension-vtk@1.1.5) (2019-12-16)
**Note:** Version bump only for package @ohif/extension-vtk
## [1.1.4](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.1.3...@ohif/extension-vtk@1.1.4) (2019-12-16)
**Note:** Version bump only for package @ohif/extension-vtk
## [1.1.3](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.1.2...@ohif/extension-vtk@1.1.3) (2019-12-13)
**Note:** Version bump only for package @ohif/extension-vtk
## [1.1.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.1.1...@ohif/extension-vtk@1.1.2) (2019-12-12)
**Note:** Version bump only for package @ohif/extension-vtk
## [1.1.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.1.0...@ohif/extension-vtk@1.1.1) (2019-12-11)
**Note:** Version bump only for package @ohif/extension-vtk
# [1.1.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.0.2...@ohif/extension-vtk@1.1.0) (2019-12-11)
### Features
* 🎸 DICOM SR STOW on MeasurementAPI ([#954](https://github.com/OHIF/Viewers/issues/954)) ([ebe1af8](https://github.com/OHIF/Viewers/commit/ebe1af8d4f75d2483eba869655906d7829bd9666)), closes [#758](https://github.com/OHIF/Viewers/issues/758)
## [1.0.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.0.1...@ohif/extension-vtk@1.0.2) (2019-12-11)
**Note:** Version bump only for package @ohif/extension-vtk
## [1.0.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.0.0...@ohif/extension-vtk@1.0.1) (2019-12-09)
**Note:** Version bump only for package @ohif/extension-vtk
# [1.0.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.54.6...@ohif/extension-vtk@1.0.0) (2019-12-09)
* feat!: Ability to configure cornerstone tools via extension configuration (#1229) ([55a5806](https://github.com/OHIF/Viewers/commit/55a580659ecb74ca6433461d8f9a05c2a2b69533)), closes [#1229](https://github.com/OHIF/Viewers/issues/1229)
### BREAKING CHANGES
* modifies the exposed react <App /> components props. The contract for providing configuration for the app has changed. Please reference updated documentation for guidance.
## [0.54.6](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.54.5...@ohif/extension-vtk@0.54.6) (2019-12-07)
**Note:** Version bump only for package @ohif/extension-vtk
+5 -6
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/extension-vtk",
"version": "0.54.6",
"version": "1.2.4",
"description": "OHIF extension for VTK.js",
"author": "OHIF",
"license": "MIT",
@@ -34,7 +34,7 @@
"@ohif/ui": "^0.50.0",
"cornerstone-core": "^2.2.8",
"cornerstone-wado-image-loader": "^3.0.0",
"dcmjs": "^0.6.1",
"dcmjs": "^0.8.2",
"dicom-parser": "^1.8.3",
"i18next": "^17.0.3",
"i18next-browser-languagedetector": "^3.0.1",
@@ -52,11 +52,10 @@
"react-vtkjs-viewport": "^0.3.9"
},
"devDependencies": {
"@ohif/core": "^1.13.3",
"@ohif/ui": "^0.65.4",
"cornerstone-tools": "^4.8.0",
"@ohif/core": "^2.3.1",
"@ohif/ui": "^1.1.7",
"cornerstone-tools": "^4.9.0",
"cornerstone-wado-image-loader": "^3.0.0",
"dcmjs": "^0.6.1",
"dicom-parser": "^1.8.3",
"gh-pages": "^2.0.1",
"i18next": "^17.0.3",
+7 -4
View File
@@ -358,10 +358,13 @@ class OHIFVTKViewport extends Component {
// TODO: Does it make more sense to use Context?
if (this.props.children && this.props.children.length) {
childrenWithProps = this.props.children.map((child, index) => {
return React.cloneElement(child, {
viewportIndex: this.props.viewportIndex,
key: index,
});
return (
child &&
React.cloneElement(child, {
viewportIndex: this.props.viewportIndex,
key: index,
})
);
});
}
@@ -0,0 +1,87 @@
import React from 'react';
import { useSelector } from 'react-redux';
import PropTypes from 'prop-types';
import { ToolbarButton } from '@ohif/ui';
import { utils } from '@ohif/core';
const { studyMetadataManager } = utils;
let isVisible = true;
const _isDisplaySetReconstructable = (viewportSpecificData = {}, activeViewportIndex) => {
if (!viewportSpecificData[activeViewportIndex]) {
return false;
};
const { displaySetInstanceUid, studyInstanceUid } = viewportSpecificData[
activeViewportIndex
];
const studies = studyMetadataManager.all();
const study = studies.find(
study => study.studyInstanceUID === studyInstanceUid
);
if (!study) {
return false;
}
const displaySet = study._displaySets.find(set => set.displaySetInstanceUid === displaySetInstanceUid);
if (!displaySet) {
return false;
};
return displaySet.isReconstructable;
};
function VTKMPRToolbarButton({
parentContext,
toolbarClickCallback,
button,
activeButtons,
isActive,
className,
}) {
const { id, label, icon } = button;
const { viewportSpecificData, activeViewportIndex } = useSelector(state => {
const { viewports = {} } = state;
const { viewportSpecificData, activeViewportIndex } = viewports;
return {
viewportSpecificData,
activeViewportIndex,
}
});
isVisible = _isDisplaySetReconstructable(
viewportSpecificData,
activeViewportIndex,
);
return (
<React.Fragment>
{isVisible && (
<ToolbarButton
key={id}
label={label}
icon={icon}
onClick={evt => toolbarClickCallback(button, evt)}
isActive={isActive}
/>
)}
</React.Fragment>
);
}
VTKMPRToolbarButton.propTypes = {
parentContext: PropTypes.object.isRequired,
toolbarClickCallback: PropTypes.func.isRequired,
button: PropTypes.object.isRequired,
activeButtons: PropTypes.array.isRequired,
isActive: PropTypes.bool,
className: PropTypes.string,
};
export default VTKMPRToolbarButton;
+12 -1
View File
@@ -1,4 +1,5 @@
import SlabThicknessToolbarComponent from './toolbarComponents/SlabThicknessToolbarComponent';
import VTKMPRToolbarButton from './toolbarComponents/VTKMPRToolbarButton';
const TOOLBAR_BUTTON_TYPES = {
COMMAND: 'command',
@@ -115,9 +116,19 @@ const definitions = [
},
],
},
{
id: '2DMPR',
label: '2D MPR',
icon: 'cube',
//
CustomComponent: VTKMPRToolbarButton,
type: TOOLBAR_BUTTON_TYPES.COMMAND,
commandName: 'mpr2d',
context: 'ACTIVE_VIEWPORT::CORNERSTONE',
},
];
export default {
definitions,
defaultContext: 'ACTIVE_VIEWPORT::VTK',
};
}
+90
View File
@@ -3,6 +3,96 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [2.3.1](https://github.com/OHIF/Viewers/compare/@ohif/core@2.3.0...@ohif/core@2.3.1) (2019-12-30)
### Bug Fixes
* 🐛 1241: Make Plugin switch part of ToolbarModule ([#1322](https://github.com/OHIF/Viewers/issues/1322)) ([6540e36](https://github.com/OHIF/Viewers/commit/6540e36818944ac2eccc696186366ae495b33a04)), closes [#1241](https://github.com/OHIF/Viewers/issues/1241)
# [2.3.0](https://github.com/OHIF/Viewers/compare/@ohif/core@2.2.1...@ohif/core@2.3.0) (2019-12-20)
### Features
* 🎸 Configuration so viewer tools can nix handles ([#1304](https://github.com/OHIF/Viewers/issues/1304)) ([63594d3](https://github.com/OHIF/Viewers/commit/63594d36b0bdba59f0901095aed70b75fb05172d)), closes [#1223](https://github.com/OHIF/Viewers/issues/1223)
## [2.2.1](https://github.com/OHIF/Viewers/compare/@ohif/core@2.2.0...@ohif/core@2.2.1) (2019-12-18)
**Note:** Version bump only for package @ohif/core
# [2.2.0](https://github.com/OHIF/Viewers/compare/@ohif/core@2.1.1...@ohif/core@2.2.0) (2019-12-16)
### Features
* 🎸 Expose extension config to modules ([#1279](https://github.com/OHIF/Viewers/issues/1279)) ([4ea239a](https://github.com/OHIF/Viewers/commit/4ea239a9535ef297e23387c186e537ab273744ea)), closes [#1268](https://github.com/OHIF/Viewers/issues/1268)
## [2.1.1](https://github.com/OHIF/Viewers/compare/@ohif/core@2.1.0...@ohif/core@2.1.1) (2019-12-16)
**Note:** Version bump only for package @ohif/core
# [2.1.0](https://github.com/OHIF/Viewers/compare/@ohif/core@2.0.2...@ohif/core@2.1.0) (2019-12-11)
### Features
* 🎸 DICOM SR STOW on MeasurementAPI ([#954](https://github.com/OHIF/Viewers/issues/954)) ([ebe1af8](https://github.com/OHIF/Viewers/commit/ebe1af8d4f75d2483eba869655906d7829bd9666)), closes [#758](https://github.com/OHIF/Viewers/issues/758)
## [2.0.2](https://github.com/OHIF/Viewers/compare/@ohif/core@2.0.1...@ohif/core@2.0.2) (2019-12-11)
**Note:** Version bump only for package @ohif/core
## [2.0.1](https://github.com/OHIF/Viewers/compare/@ohif/core@2.0.0...@ohif/core@2.0.1) (2019-12-09)
**Note:** Version bump only for package @ohif/core
# [2.0.0](https://github.com/OHIF/Viewers/compare/@ohif/core@1.13.3...@ohif/core@2.0.0) (2019-12-09)
* feat!: Ability to configure cornerstone tools via extension configuration (#1229) ([55a5806](https://github.com/OHIF/Viewers/commit/55a580659ecb74ca6433461d8f9a05c2a2b69533)), closes [#1229](https://github.com/OHIF/Viewers/issues/1229)
### BREAKING CHANGES
* modifies the exposed react <App /> components props. The contract for providing configuration for the app has changed. Please reference updated documentation for guidance.
## [1.13.3](https://github.com/OHIF/Viewers/compare/@ohif/core@1.13.2...@ohif/core@1.13.3) (2019-12-06)
**Note:** Version bump only for package @ohif/core
+3 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/core",
"version": "1.13.3",
"version": "2.3.1",
"description": "Generic business logic for web-based medical imaging applications",
"author": "OHIF Core Team",
"license": "MIT",
@@ -32,13 +32,14 @@
},
"peerDependencies": {
"cornerstone-core": "^2.2.8",
"cornerstone-tools": "^4.8.0",
"cornerstone-tools": "^4.9.0",
"cornerstone-wado-image-loader": "^3.0.0",
"dicom-parser": "^1.8.3"
},
"dependencies": {
"@babel/runtime": "^7.5.5",
"ajv": "^6.10.0",
"dcmjs": "^0.8.3",
"dicomweb-client": "^0.5.2",
"isomorphic-base64": "^1.0.2",
"lodash.clonedeep": "^4.5.0",
+80
View File
@@ -0,0 +1,80 @@
import log from '../log';
import studies from '../studies';
import utils from '../utils';
import {
retrieveMeasurementFromSR,
stowSRFromMeasurements,
} from './handleStructuredReport';
import findMostRecentStructuredReport from './utils/findMostRecentStructuredReport';
/**
*
* @typedef serverType
* @property {string} type - type of the server
* @property {string} wadoRoot - server wado root url
*
*/
/**
* Function to be registered into MeasurementAPI to retrieve measurements from DICOM Structured Reports
*
* @param {serverType} server
* @returns {Promise} Should resolve with OHIF measurementData object
*/
const retrieveMeasurements = server => {
log.info('[DICOMSR] retrieveMeasurements');
if (!server || server.type !== 'dicomWeb') {
log.error('[DICOMSR] DicomWeb server is required!');
return Promise.reject({});
}
const serverUrl = server.wadoRoot;
const studies = utils.studyMetadataManager.all();
const latestSeries = findMostRecentStructuredReport(studies);
if (!latestSeries) return Promise.resolve({});
return retrieveMeasurementFromSR(latestSeries, studies, serverUrl);
};
/**
* Function to be registered into MeasurementAPI to store measurements into DICOM Structured Reports
*
* @param {Object} measurementData - OHIF measurementData object
* @param {Object} filter
* @param {serverType} server
* @returns {Object} With message to be displayed on success
*/
const storeMeasurements = async (measurementData, filter, server) => {
log.info('[DICOMSR] storeMeasurements');
if (!server || server.type !== 'dicomWeb') {
log.error('[DICOMSR] DicomWeb server is required!');
return Promise.reject({});
}
const serverUrl = server.wadoRoot;
const firstMeasurementKey = Object.keys(measurementData)[0];
const firstMeasurement = measurementData[firstMeasurementKey][0];
const studyInstanceUid =
firstMeasurement && firstMeasurement.studyInstanceUid;
try {
await stowSRFromMeasurements(measurementData, serverUrl);
if (studyInstanceUid) {
studies.deleteStudyMetadataPromise(studyInstanceUid);
}
return {
message: 'Measurements were saved with success',
};
} catch (error) {
log.error(
`[DICOMSR] Error while saving the measurements: ${error.message}`
);
throw new Error('Error while saving the measurements.');
}
};
export { retrieveMeasurements, storeMeasurements };
@@ -0,0 +1,84 @@
import * as dcmjs from 'dcmjs';
import { api } from 'dicomweb-client';
import DICOMWeb from '../DICOMWeb';
import parseDicomStructuredReport from './parseDicomStructuredReport';
import parseMeasurementsData from './parseMeasurementsData';
import getAllDisplaySets from './utils/getAllDisplaySets';
const VERSION_NAME = 'dcmjs-0.0';
const TRANSFER_SYNTAX_UID = '1.2.840.10008.1.2.1';
/**
* Function to retrieve measurements from DICOM Structured Reports coming from determined server
*
* @param {Array} series - List of all series metaData loaded
* @param {Array} studies - List of all studies metaData loaded
* @param {string} serverUrl - Server URL to be used on request
* @returns {Object} MeasurementData
*/
const retrieveMeasurementFromSR = async (series, studies, serverUrl) => {
const config = {
url: serverUrl,
headers: DICOMWeb.getAuthorizationHeader(),
};
const dicomWeb = new api.DICOMwebClient(config);
const instance = series.getFirstInstance();
const options = {
studyInstanceUID: instance.getStudyInstanceUID(),
seriesInstanceUID: instance.getSeriesInstanceUID(),
sopInstanceUID: instance.getSOPInstanceUID(),
};
const part10SRArrayBuffer = await dicomWeb.retrieveInstance(options);
const displaySets = getAllDisplaySets(studies);
const measurementsData = parseDicomStructuredReport(
part10SRArrayBuffer,
displaySets
);
return measurementsData;
};
/**
* Function to store measurements to DICOM Structured Reports in determined server
*
* @param {Object} measurements - OHIF measurementData object
* @param {string} serverUrl - Server URL to be used on request
* @returns {Promise}
*/
const stowSRFromMeasurements = async (measurements, serverUrl) => {
const { dataset } = parseMeasurementsData(measurements);
const { DicomMetaDictionary, DicomDict } = dcmjs.data;
const meta = {
FileMetaInformationVersion: dataset._meta.FileMetaInformationVersion.Value,
MediaStorageSOPClassUID: dataset.SOPClassUID,
MediaStorageSOPInstanceUID: dataset.SOPInstanceUID,
TransferSyntaxUID: TRANSFER_SYNTAX_UID,
ImplementationClassUID: DicomMetaDictionary.uid(),
ImplementationVersionName: VERSION_NAME,
};
const denaturalized = DicomMetaDictionary.denaturalizeDataset(meta);
const dicomDict = new DicomDict(denaturalized);
dicomDict.dict = DicomMetaDictionary.denaturalizeDataset(dataset);
const part10Buffer = dicomDict.write();
const config = {
url: serverUrl,
headers: DICOMWeb.getAuthorizationHeader(),
};
const dicomWeb = new api.DICOMwebClient(config);
const options = {
datasets: [part10Buffer],
};
await dicomWeb.storeInstances(options);
};
export { retrieveMeasurementFromSR, stowSRFromMeasurements };
+10
View File
@@ -0,0 +1,10 @@
import { retrieveMeasurements, storeMeasurements } from './dataExchange';
import isToolSupported from './utils/isToolSupported';
const DICOMSR = {
retrieveMeasurements,
storeMeasurements,
isToolSupported,
};
export default DICOMSR;
@@ -0,0 +1,95 @@
import * as dcmjs from 'dcmjs';
import findInstanceMetadataBySopInstanceUid from './utils/findInstanceMetadataBySopInstanceUid';
/**
* Function to parse the part10 array buffer that comes from a DICOM Structured report into measurementData
* measurementData format is a viewer specific format to be stored into the redux and consumed by other components
* (e.g. measurement table)
*
* @param {ArrayBuffer} part10SRArrayBuffer
* @param {Array} displaySets
* @returns
*/
const parseDicomStructuredReport = (part10SRArrayBuffer, displaySets) => {
// Get the dicom data as an Object
const dicomData = dcmjs.data.DicomMessage.readFile(part10SRArrayBuffer);
const dataset = dcmjs.data.DicomMetaDictionary.naturalizeDataset(
dicomData.dict
);
const { MeasurementReport } = dcmjs.adapters.Cornerstone;
const storedMeasurementByToolType = MeasurementReport.generateToolState(
dataset
);
const measurementData = {};
let measurementNumber = 0;
Object.keys(storedMeasurementByToolType).forEach(toolName => {
const measurements = storedMeasurementByToolType[toolName];
measurementData[toolName] = [];
measurements.forEach(measurement => {
const instanceMetadata = findInstanceMetadataBySopInstanceUid(
displaySets,
measurement.sopInstanceUid
);
const { _study: study, _series: series } = instanceMetadata;
const { studyInstanceUid, patientId } = study;
const { seriesInstanceUid } = series;
const { sopInstanceUid, frameIndex } = measurement;
const imagePath = getImagePath(
studyInstanceUid,
seriesInstanceUid,
sopInstanceUid,
frameIndex
);
const imageId = instanceMetadata.getImageId();
if (!imageId) {
return;
}
// TODO: We need the currentTimepointID set into the viewer
const currentTimepointId = 'TimepointId';
const toolData = Object.assign({}, measurement, {
imageId,
imagePath,
seriesInstanceUid,
studyInstanceUid,
patientId,
measurementNumber: ++measurementNumber,
timepointId: currentTimepointId,
toolType: toolName,
_id: imageId + measurementNumber,
});
measurementData[toolName].push(toolData);
});
});
return measurementData;
};
/**
* Function to create imagePath with all imageData related
*
* @param {string} studyInstanceUid
* @param {string} seriesInstanceUid
* @param {string} sopInstanceUid
* @param {string} frameIndex
* @returns
*/
const getImagePath = (
studyInstanceUid,
seriesInstanceUid,
sopInstanceUid,
frameIndex
) => {
return [studyInstanceUid, seriesInstanceUid, sopInstanceUid, frameIndex].join(
'_'
);
};
export default parseDicomStructuredReport;
@@ -0,0 +1,57 @@
import * as dcmjs from 'dcmjs';
import cornerstone from 'cornerstone-core';
import log from '../log';
import measurements from '../measurements';
import isToolSupported from './utils/isToolSupported';
/**
* Function to parse OHIF viewer measurementData into a dcmjs MeasurementReport
*
* @param {Object} measurementsData - OHIF measurementData object
* @returns {Object} Dataset: measurement report from dcmjs
*/
const parseMeasurementsData = measurementsData => {
const { MeasurementReport } = dcmjs.adapters.Cornerstone;
const { getImageIdForImagePath } = measurements;
const toolState = {};
const unsupportedTools = [];
Object.keys(measurementsData).forEach(measurementType => {
const annotations = measurementsData[measurementType];
annotations.forEach(annotation => {
const { toolType, imagePath } = annotation;
if (isToolSupported(toolType)) {
const imageId = getImageIdForImagePath(imagePath);
toolState[imageId] = toolState[imageId] || {};
toolState[imageId][toolType] = toolState[imageId][toolType] || {
data: [],
};
toolState[imageId][toolType].data.push(annotation);
} else {
unsupportedTools.push(toolType);
}
});
});
if (unsupportedTools.length > 0) {
log.warn(
`[DICOMSR] Tooltypes not supported: ${unsupportedTools.join(', ')}`
);
}
const report = MeasurementReport.generateReport(
toolState,
cornerstone.metaData
);
return {
dataset: report.dataset,
};
};
export default parseMeasurementsData;
@@ -0,0 +1,24 @@
/**
* Should Find the requested instance metadata into the displaySets and return
*
* @param {Array} displaySets - List of displaySets
* @param {string} sopInstanceUid - sopInstanceUID to look for
* @returns {Object} - instance metadata found
*/
const findInstanceMetadataBySopInstanceUid = (displaySets, sopInstanceUid) => {
let instanceFound;
displaySets.find(displaySet => {
if (!displaySet.images) return false;
instanceFound = displaySet.images.find(
instanceMetadata => instanceMetadata._sopInstanceUID === sopInstanceUid
);
return !!instanceFound;
});
return instanceFound;
};
export default findInstanceMetadataBySopInstanceUid;
@@ -0,0 +1,60 @@
/**
* Should find the most recent Structured Report metadata
*
* @param {Array} studies
* @returns {Object} Series
*/
const findMostRecentStructuredReport = studies => {
let mostRecentStructuredReport;
studies.forEach(study => {
const allSeries = study.getSeries ? study.getSeries() : [];
allSeries.forEach(series => {
if (isStructuredReportSeries(series)) {
if (
!mostRecentStructuredReport ||
compareSeriesDate(series, mostRecentStructuredReport)
) {
mostRecentStructuredReport = series;
}
}
});
});
return mostRecentStructuredReport;
};
/**
* Checks if series sopClassUID matches with the supported Structured Reports sopClassUID
*
* @param {Object} series - Series metadata
* @returns {boolean}
*/
const isStructuredReportSeries = series => {
const supportedSopClassUIDs = [
'1.2.840.10008.5.1.4.1.1.88.22',
'1.2.840.10008.5.1.4.1.1.11.1',
];
const firstInstance = series.getFirstInstance();
const sopClassUid = firstInstance._instance.sopClassUid;
return supportedSopClassUIDs.includes(sopClassUid);
};
/**
* Checkes if series1 is newer than series2
*
* @param {Object} series1 - Series Metadata 1
* @param {Object} series2 - Series Metadata 2
* @returns {boolean} true/false if series1 is newer than series2
*/
const compareSeriesDate = (series1, series2) => {
return (
series1._data.seriesDate > series2._data.seriesDate ||
(series1._data.seriesDate === series2._data.seriesDate &&
series1._data.seriesTime > series2._data.seriesTime)
);
};
export default findMostRecentStructuredReport;
@@ -0,0 +1,19 @@
/**
* Retrieve a list of all displaySets of all studies
*
* @param {Object} studies - List of studies loaded into the viewer
* @returns {Object} List of DisplaySets
*/
const getAllDisplaySets = studies => {
let allDisplaySets = [];
studies.forEach(study => {
if (study.getDisplaySets) {
allDisplaySets = allDisplaySets.concat(study.getDisplaySets());
}
});
return allDisplaySets;
};
export default getAllDisplaySets;
@@ -0,0 +1,14 @@
import * as dcmjs from 'dcmjs';
/**
* Checks if dcmjs has support to determined tool
*
* @param {string} toolName
* @returns {boolean}
*/
const isToolSupported = toolName => {
const adapter = dcmjs.adapters.Cornerstone;
return !!adapter[toolName];
};
export default isToolSupported;
+1 -1
View File
@@ -59,7 +59,7 @@ export class CommandsManager {
*
* @method
* @param {string} contextName - Namespace for commands
* @returs {Object} - the matched context
* @returns {Object} - the matched context
*/
getContext(contextName) {
const context = this.contexts[contextName];
@@ -2,13 +2,14 @@ import MODULE_TYPES from './MODULE_TYPES.js';
import log from './../log.js';
export default class ExtensionManager {
constructor({ commandsManager, servicesManager }) {
constructor({ commandsManager, servicesManager, appConfig = {} }) {
this.modules = {};
this.registeredExtensionIds = [];
this.moduleTypeNames = Object.values(MODULE_TYPES);
//
this._commandsManager = commandsManager;
this._servicesManager = servicesManager;
this._appConfig = appConfig;
this.moduleTypeNames.forEach(moduleType => {
this.modules[moduleType] = [];
@@ -26,7 +27,7 @@ export default class ExtensionManager {
const hasConfiguration = Array.isArray(extension);
if (hasConfiguration) {
const [ohifExtension, configuration] = extensions;
const [ohifExtension, configuration] = extension;
this.registerExtension(ohifExtension, configuration);
} else {
this.registerExtension(extension);
@@ -70,6 +71,7 @@ export default class ExtensionManager {
extension.preRegistration({
servicesManager: this._servicesManager,
commandsManager: this._commandsManager,
appConfig: this._appConfig,
configuration,
});
}
@@ -79,7 +81,8 @@ export default class ExtensionManager {
const extensionModule = this._getExtensionModule(
moduleType,
extension,
extensionId
extensionId,
configuration
);
if (extensionModule) {
@@ -102,7 +105,7 @@ export default class ExtensionManager {
* @param {Object} extension
* @param {string} extensionId - Used for logging warnings
*/
_getExtensionModule(moduleType, extension, extensionId) {
_getExtensionModule(moduleType, extension, extensionId, configuration) {
const getModuleFnName = 'get' + _capitalizeFirstCharacter(moduleType);
const getModuleFn = extension[getModuleFnName];
@@ -114,6 +117,8 @@ export default class ExtensionManager {
const extensionModule = getModuleFn({
servicesManager: this._servicesManager,
commandsManager: this._commandsManager,
appConfig: this._appConfig,
configuration,
});
if (!extensionModule) {
@@ -6,7 +6,7 @@ import log from './../log.js';
jest.mock('./../log.js');
describe('ExtensionManager.js', () => {
let extensionManager, commandsManager;
let extensionManager, commandsManager, servicesManager, appConfig;
beforeEach(() => {
commandsManager = {
@@ -14,7 +14,17 @@ describe('ExtensionManager.js', () => {
getContext: jest.fn(),
registerCommand: jest.fn(),
};
extensionManager = new ExtensionManager({ commandsManager });
servicesManager = {
registerService: jest.fn(),
};
appConfig = {
testing: true,
};
extensionManager = new ExtensionManager({
servicesManager,
commandsManager,
appConfig,
});
log.warn.mockClear();
jest.clearAllMocks();
});
@@ -37,6 +47,24 @@ describe('ExtensionManager.js', () => {
// Assert
expect(extensionManager.registerExtension.mock.calls.length).toBe(3);
});
it('calls registerExtension() for each extension passing its configuration if tuple', () => {
const fakeConfiguration = { testing: true };
extensionManager.registerExtension = jest.fn();
// SUT
const fakeExtensions = [
{ one: '1' },
[{ two: '2' }, fakeConfiguration],
{ three: '3 ' },
];
extensionManager.registerExtensions(fakeExtensions);
// Assert
expect(extensionManager.registerExtension.mock.calls[1][1]).toEqual(
fakeConfiguration
);
});
});
describe('registerExtension()', () => {
@@ -49,19 +77,19 @@ describe('ExtensionManager.js', () => {
expect(fakeExtension.preRegistration.mock.calls.length).toBe(1);
});
it('calls preRegistration() passing configuration along with servicesManager and commandsManager instances for extension', () => {
const configuration = { config: 'Some configuration' };
extensionManager._servicesManager = { services: { TestService: {} } };
it('calls preRegistration() passing dependencies and extension configuration to extension', () => {
const extensionConfiguration = { config: 'Some configuration' };
// SUT
const fakeExtension = { one: '1', preRegistration: jest.fn() };
extensionManager.registerExtension(fakeExtension, configuration);
const extension = { one: '1', preRegistration: jest.fn() };
extensionManager.registerExtension(extension, extensionConfiguration);
// Assert
expect(fakeExtension.preRegistration.mock.calls[0][0]).toEqual({
servicesManager: extensionManager._servicesManager,
commandsManager: extensionManager._commandsManager,
configuration,
expect(extension.preRegistration.mock.calls[0][0]).toEqual({
servicesManager,
commandsManager,
appConfig,
configuration: extensionConfiguration,
});
});
@@ -135,8 +163,8 @@ describe('ExtensionManager.js', () => {
);
});
it('successfully passes a servicesManager and commandsManager instances to each module', () => {
extensionManager._servicesManager = { services: { TestService: {} } };
it('successfully passes dependencies to each module along with extension configuration', () => {
const extensionConfiguration = { testing: true };
const extension = {
id: 'hello-world',
@@ -147,11 +175,17 @@ describe('ExtensionManager.js', () => {
getCommandsModule: jest.fn(),
};
extensionManager.registerExtension(extension);
extensionManager.registerExtension(extension, extensionConfiguration);
expect(extension.getViewportModule.mock.calls[0][0]).toEqual({
servicesManager: extensionManager._servicesManager,
commandsManager: extensionManager._commandsManager,
Object.keys(extension).forEach(module => {
if (typeof extension[module] === 'function') {
expect(extension[module].mock.calls[0][0]).toEqual({
servicesManager,
commandsManager,
appConfig,
configuration: extensionConfiguration,
});
}
});
});
@@ -192,7 +226,7 @@ describe('ExtensionManager.js', () => {
return {
definitions: {
exampleDefinition: {
commandFn: () => {},
commandFn: () => { },
storeContexts: [],
options: {},
},
+12 -9
View File
@@ -5,6 +5,7 @@ import { ServicesManager } from './services';
import classes, { CommandsManager, HotkeysManager } from './classes/';
import DICOMWeb from './DICOMWeb';
import DICOMSR from './DICOMSR';
import cornerstone from './cornerstone.js';
import hangingProtocols from './hanging-protocols';
import header from './header.js';
@@ -20,9 +21,9 @@ import user from './user.js';
import utils from './utils/';
import {
createUINotificationService,
createUIModalService,
createUIDialogService,
UINotificationService,
UIModalService,
UIDialogService,
} from './services';
const OHIF = {
@@ -46,13 +47,14 @@ const OHIF = {
object,
log,
DICOMWeb,
DICOMSR,
viewer: {},
measurements,
hangingProtocols,
//
createUINotificationService,
createUIModalService,
createUIDialogService,
UINotificationService,
UIModalService,
UIDialogService,
};
export {
@@ -76,12 +78,13 @@ export {
object,
log,
DICOMWeb,
DICOMSR,
measurements,
hangingProtocols,
//
createUINotificationService,
createUIModalService,
createUIDialogService,
UINotificationService,
UIModalService,
UIDialogService,
};
export { OHIF };
+4 -3
View File
@@ -10,9 +10,9 @@ describe('Top level exports', () => {
'HotkeysManager',
'ServicesManager',
//
'createUINotificationService',
'createUIModalService',
'createUIDialogService',
'UINotificationService',
'UIModalService',
'UIDialogService',
//
'utils',
'studies',
@@ -28,6 +28,7 @@ describe('Top level exports', () => {
'object',
'log',
'DICOMWeb',
'DICOMSR',
'OHIF', //
'measurements',
'hangingProtocols',
@@ -1,4 +1,5 @@
import cornerstoneTools from 'cornerstone-tools';
import cornerstone from 'cornerstone-core';
import log from '../../log';
import getLabel from '../lib/getLabel';
import getDescription from '../lib/getDescription';
@@ -7,13 +8,32 @@ import guid from '../../utils/guid';
import studyMetadataManager from '../../utils/studyMetadataManager';
import { measurementApiDefaultConfig } from './../configuration.js';
const configuration = {
...measurementApiDefaultConfig,
...measurementApiDefaultConfig
};
export default class MeasurementApi {
static Instance;
/**
* Set configuration: It should merge default configuration with any new one
*
* @static
* @param {Object} config
* @param {Object} config.server
* @param {string} config.server.type - The server type
* @param {string} config.server.wadoRoot - The server wado URL root
* @param {Array} config.measurementTools
* @param {string} config.measurementTools[].id - The tool group id
* @param {string} config.measurementTools[].name - The tool group name
* @param {Array} config.measurementTools[].childTools - The child tool's configuration
* @param {Object} config.dataExchange
* @param {Function} config.dataExchange.store - Function that store measurement data
* @param {Function} config.dataExchange.retrieve - Function that retrieves measurement data
*
* @memberof MeasurementApi
*/
static setConfiguration(config) {
Object.assign(configuration, config);
}
@@ -203,13 +223,14 @@ export default class MeasurementApi {
retrieveMeasurements(patientId, timepointIds) {
const retrievalFn = configuration.dataExchange.retrieve;
const { server } = configuration;
if (typeof retrievalFn !== 'function') {
log.error('Measurement retrieval function has not been configured.');
return;
}
return new Promise((resolve, reject) => {
retrievalFn(patientId, timepointIds).then(measurementData => {
retrievalFn(server).then(measurementData => {
if (measurementData) {
log.info('Measurement data retrieval');
log.info(measurementData);
@@ -230,6 +251,10 @@ export default class MeasurementApi {
// Synchronize the new tool data
this.syncMeasurementsAndToolData();
cornerstone.getEnabledElements().forEach(enabledElement => {
cornerstone.updateImage(enabledElement.element);
});
// Let others know that the measurements are updated
this.onMeasurementsUpdated();
}, reject);
@@ -237,6 +262,7 @@ export default class MeasurementApi {
}
storeMeasurements(timepointId) {
const { server } = configuration;
const storeFn = configuration.dataExchange.store;
if (typeof storeFn !== 'function') {
log.error('Measurement store function has not been configured.');
@@ -278,8 +304,9 @@ export default class MeasurementApi {
};
log.info('Saving Measurements for timepoints:', timepoints);
return storeFn(measurementData, filter).then(() => {
return storeFn(measurementData, filter, server).then(result => {
log.info('Measurement storage completed');
return result;
});
}
+28 -11
View File
@@ -7,10 +7,12 @@ export default class ServicesManager {
}
/**
* Registers a new service.
*
* @param {Object} service
* @param {Object} configuration
*/
registerService(service) {
registerService(service, configuration = {}) {
if (!service) {
log.warn(
'Attempting to register a null/undefined service. Exiting early.'
@@ -18,32 +20,47 @@ export default class ServicesManager {
return;
}
let serviceName = service.name;
if (!serviceName) {
if (!service.name) {
log.warn(`Service name not set. Exiting early.`);
return;
}
if (this.registeredServiceNames.includes(serviceName)) {
if (this.registeredServiceNames.includes(service.name)) {
log.warn(
`Extension name ${serviceName} has already been registered. Exiting before duplicating services.`
`Service name ${service.name} has already been registered. Exiting before duplicating services.`
);
return;
}
this.services[service.name] = service;
if (service.create) {
this.services[service.name] = service.create({
configuration,
});
} else {
log.warn(`Service create factory function not defined. Exiting early.`);
return;
}
// Track service registration
this.registeredServiceNames.push(serviceName);
/* Track service registration */
this.registeredServiceNames.push(service.name);
}
/**
* An array of services.
* An array of services, or an array of arrays that contains service
* configuration pairs.
*
* @param {Object[]} services - Array of services
*/
registerServices(services) {
services.forEach(service => this.registerService(service));
services.forEach(service => {
const hasConfiguration = Array.isArray(service);
if (hasConfiguration) {
const [ohifService, configuration] = service;
this.registerService(ohifService, configuration);
} else {
this.registerService(service);
}
});
}
}
@@ -16,18 +16,32 @@ describe('ServicesManager.js', () => {
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);
servicesManager.registerServices([
{ name: 'UINotificationTestService', create: jest.fn() },
{ name: 'UIModalTestService', create: jest.fn() },
]);
expect(servicesManager.registerService.mock.calls.length).toBe(2);
});
it('calls registerService() for each service passing its configuration if tuple', () => {
servicesManager.registerService = jest.fn();
const fakeConfiguration = { testing: true };
servicesManager.registerServices([
{ name: 'UINotificationTestService', create: jest.fn() },
[{ name: 'UIModalTestService', create: jest.fn() }, fakeConfiguration],
]);
expect(servicesManager.registerService.mock.calls[1][1]).toEqual(
fakeConfiguration
);
});
});
describe('registerService()', () => {
const fakeService = { name: 'UINotificationService', create: jest.fn() };
it('logs a warning if the service is null or undefined', () => {
const undefinedService = undefined;
const nullService = null;
@@ -39,8 +53,8 @@ describe('ServicesManager.js', () => {
});
it('logs a warning if the service does not have a name', () => {
const serviceWithEmptyName = { name: '', hide: jest.fn() };
const serviceWithoutName = { hide: jest.fn() };
const serviceWithEmptyName = { name: '', create: jest.fn() };
const serviceWithoutName = { create: jest.fn() };
servicesManager.registerService(serviceWithEmptyName);
servicesManager.registerService(serviceWithoutName);
@@ -48,23 +62,37 @@ describe('ServicesManager.js', () => {
expect(log.warn.mock.calls.length).toBe(2);
});
it('tracks which services have been registered', () => {
const service = {
name: 'UINotificationService',
};
it('logs a warning if the service does not have a create factory function', () => {
const serviceWithoutCreate = { 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);
servicesManager.registerService(serviceWithoutCreate);
expect(log.warn.mock.calls.length).toBe(1);
});
it('tracks which services have been registered', () => {
servicesManager.registerService(fakeService);
expect(servicesManager.registeredServiceNames).toContain(
fakeService.name
);
});
it('logs a warning if the service has an name that has already been registered', () => {
servicesManager.registerService(fakeService);
servicesManager.registerService(fakeService);
expect(log.warn.mock.calls.length).toBe(1);
});
it('pass dependencies and configuration to service create factory function', () => {
const configuration = { config: 'Some configuration' };
servicesManager.registerService(fakeService, configuration);
expect(fakeService.create.mock.calls[0][0]).toEqual({
configuration,
});
});
});
});
@@ -17,59 +17,60 @@
* @property {Object} contentProps The dialog content props.
* @property {boolean} [isDraggable=true] Controls if dialog content is draggable or not.
* @property {boolean} [showOverlay=false] Controls dialog overlay.
* @property {boolean} [centralize=false] Center the dialog on the screen.
* @property {boolean} [preservePosition=true] Use last position instead of default.
* @property {ElementPosition} defaultPosition Specifies the `x` and `y` that the dragged item should start at.
* @property {ElementPosition} position If this property is present, the item becomes 'controlled' and is not responsive to user input.
* @property {Function} onStart Called when dragging starts. If `false` is returned any handler, the action will cancel.
* @property {Function} onStop Called when dragging stops.
* @property {Function} onDrag Called while dragging.
*/
const uiDialogServicePublicAPI = {
name: 'UIDialogService',
dismiss,
dismissAll,
create,
const name = 'UIDialogService';
const publicAPI = {
name,
dismiss: _dismiss,
dismissAll: _dismissAll,
create: _create,
setServiceImplementation,
};
const uiDialogServiceImplementation = {
const serviceImplementation = {
_dismiss: () => console.warn('dismiss() NOT IMPLEMENTED'),
_dismissAll: () => console.warn('dismissAll() NOT IMPLEMENTED'),
_create: () => console.warn('create() NOT IMPLEMENTED'),
};
function createUIDialogService() {
return uiDialogServicePublicAPI;
}
/**
* Show a new UI dialog;
*
* @param {DialogProps} props { id, content, contentProps, onStart, onDrag, onStop, isDraggable, showOverlay, defaultPosition, position }
* @param {DialogProps} props { id, content, contentProps, onStart, onDrag, onStop, centralize, isDraggable, showOverlay, preservePosition, defaultPosition }
*/
function create({
function _create({
id,
content,
contentProps,
onStart,
onDrag,
onStop,
centralize = false,
preservePosition = true,
isDraggable = true,
showOverlay = false,
defaultPosition,
position,
}) {
return uiDialogServiceImplementation._create({
return serviceImplementation._create({
id,
content,
contentProps,
onStart,
onDrag,
onStop,
centralize,
preservePosition,
isDraggable,
showOverlay,
defaultPosition,
position,
});
}
@@ -78,8 +79,8 @@ function create({
*
* @returns void
*/
function dismissAll() {
return uiDialogServiceImplementation._dismissAll();
function _dismissAll() {
return serviceImplementation._dismissAll();
}
/**
@@ -87,8 +88,8 @@ function dismissAll() {
*
* @returns void
*/
function dismiss({ id }) {
return uiDialogServiceImplementation._dismiss({ id });
function _dismiss({ id }) {
return serviceImplementation._dismiss({ id });
}
/**
@@ -106,14 +107,19 @@ function setServiceImplementation({
create: createImplementation,
}) {
if (dismissImplementation) {
uiDialogServiceImplementation._dismiss = dismissImplementation;
serviceImplementation._dismiss = dismissImplementation;
}
if (dismissAllImplementation) {
uiDialogServiceImplementation._dismissAll = dismissAllImplementation;
serviceImplementation._dismissAll = dismissAllImplementation;
}
if (createImplementation) {
uiDialogServiceImplementation._create = createImplementation;
serviceImplementation._create = createImplementation;
}
}
export default createUIDialogService;
export default {
name,
create: ({ configuration = {} }) => {
return publicAPI;
},
};
@@ -11,28 +11,26 @@
* @property {string} [customClassName=null] The custom class to style the modal.
*/
const uiModalServicePublicAPI = {
name: 'UIModalService',
hide,
show,
const name = 'UIModalService';
const publicAPI = {
name,
hide: _hide,
show: _show,
setServiceImplementation,
};
const uiModalServiceImplementation = {
const serviceImplementation = {
_hide: () => console.warn('hide() NOT IMPLEMENTED'),
_show: () => console.warn('show() NOT IMPLEMENTED'),
};
function createUIModalService() {
return uiModalServicePublicAPI;
}
/**
* Show a new UI modal;
*
* @param {ModalProps} props { content, contentProps, shouldCloseOnEsc, isOpen, closeButton, title, customClassName }
*/
function show({
function _show({
content = null,
contentProps = null,
shouldCloseOnEsc = false,
@@ -41,7 +39,7 @@ function show({
title = null,
customClassName = null,
}) {
return uiModalServiceImplementation._show({
return serviceImplementation._show({
content,
contentProps,
shouldCloseOnEsc,
@@ -57,8 +55,8 @@ function show({
*
* @returns void
*/
function hide() {
return uiModalServiceImplementation._hide();
function _hide() {
return serviceImplementation._hide();
}
/**
@@ -74,11 +72,16 @@ function setServiceImplementation({
show: showImplementation,
}) {
if (hideImplementation) {
uiModalServiceImplementation._hide = hideImplementation;
serviceImplementation._hide = hideImplementation;
}
if (showImplementation) {
uiModalServiceImplementation._show = showImplementation;
serviceImplementation._show = showImplementation;
}
}
export default createUIModalService;
export default {
name,
create: ({ configuration = {} }) => {
return publicAPI;
},
};
@@ -10,22 +10,20 @@
* @property {boolean} [autoClose=true]
*/
const uiNotificationServicePublicAPI = {
name: 'UINotificationService',
hide,
show,
const name = 'UINotificationService';
const publicAPI = {
name,
hide: _hide,
show: _show,
setServiceImplementation,
};
const uiNotificationServiceImplementation = {
const serviceImplementation = {
_hide: () => console.warn('hide() NOT IMPLEMENTED'),
_show: () => console.warn('show() NOT IMPLEMENTED'),
};
function createUINotificationService() {
return uiNotificationServicePublicAPI;
}
/**
* Create and show a new UI notification; returns the
* ID of the created notification.
@@ -33,7 +31,7 @@ function createUINotificationService() {
* @param {Notification} notification { title, message, duration, position, type, autoClose}
* @returns {number} id
*/
function show({
function _show({
title,
message,
duration = 5000,
@@ -41,7 +39,7 @@ function show({
type = 'info',
autoClose = true,
}) {
return uiNotificationServiceImplementation._show({
return serviceImplementation._show({
title,
message,
duration,
@@ -57,8 +55,8 @@ function show({
* @param {number} id - id of the notification to hide/dismiss
* @returns undefined
*/
function hide(id) {
return uiNotificationServiceImplementation._hide({ id });
function _hide(id) {
return serviceImplementation._hide({ id });
}
/**
@@ -74,11 +72,16 @@ function setServiceImplementation({
show: showImplementation,
}) {
if (hideImplementation) {
uiNotificationServiceImplementation._hide = hideImplementation;
serviceImplementation._hide = hideImplementation;
}
if (showImplementation) {
uiNotificationServiceImplementation._show = showImplementation;
serviceImplementation._show = showImplementation;
}
}
export default createUINotificationService;
export default {
name,
create: ({ configuration = {} }) => {
return publicAPI;
},
};
+6 -6
View File
@@ -1,11 +1,11 @@
import ServicesManager from './ServicesManager.js';
import createUINotificationService from './UINotificationService';
import createUIModalService from './UIModalService';
import createUIDialogService from './UIDialogService';
import UINotificationService from './UINotificationService';
import UIModalService from './UIModalService';
import UIDialogService from './UIDialogService';
export {
createUINotificationService,
createUIModalService,
createUIDialogService,
UINotificationService,
UIModalService,
UIDialogService,
ServicesManager,
};
@@ -40,7 +40,9 @@ export default class RetrieveMetadataLoader {
if (result && result.length) {
break; // closes iterator in case data is retrieved successfully
}
} catch (e) { }
} catch (e) {
throw e;
}
}
if (loaders.next().done && !result) {
@@ -51,8 +53,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) {}
}
+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.
## [0.52.4](https://github.com/OHIF/Viewers/compare/@ohif/i18n@0.52.3...@ohif/i18n@0.52.4) (2019-12-16)
**Note:** Version bump only for package @ohif/i18n
## [0.52.3](https://github.com/OHIF/Viewers/compare/@ohif/i18n@0.52.2...@ohif/i18n@0.52.3) (2019-12-12)
### Bug Fixes
* translations ([#1234](https://github.com/OHIF/Viewers/issues/1234)) ([30b9e44](https://github.com/OHIF/Viewers/commit/30b9e4422073557287ef26a80b38eeb3f3fcff4c))
## [0.52.2](https://github.com/OHIF/Viewers/compare/@ohif/i18n@0.52.1...@ohif/i18n@0.52.2) (2019-11-28)
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/i18n",
"version": "0.52.2",
"version": "0.52.4",
"description": "Internationalization library for The OHIF Viewer",
"author": "OHIF",
"license": "MIT",
+9 -8
View File
@@ -29,19 +29,20 @@ function addLocales(newLocales) {
customDebug(resourceBundle, 'info');
}
// Note: Developers can add the API key to use the in-context editor.
// DO NOT commit the API key
const config = (window.config && window.config.i18n) || {};
/*
* Note: Developers can add the API key to use the
* in-context editor using environment variables.
* (DO NOT commit the API key)
*/
const locizeOptions = {
projectId: config.LOCIZE_PROJECTID,
apiKey: config.LOCIZE_API_KEY,
projectId: process.env.LOCIZE_PROJECTID,
apiKey: process.env.LOCIZE_API_KEY,
referenceLng: 'en-US',
fallbacklng: 'en-US',
};
const envUseLocize = !!config.USE_LOCIZE;
const envApiKeyAvailable = !!config.LOCIZE_API_KEY;
const envUseLocize = !!process.env.USE_LOCIZE;
const envApiKeyAvailable = !!process.env.LOCIZE_API_KEY;
const DEFAULT_LANGUAGE = 'en-US';
function initI18n(
@@ -10,6 +10,7 @@
"Clear": "Clear",
"Coronal": "Coronal",
"Crosshairs": "Crosshairs",
"Download": "Download",
"Ellipse": "Ellipse",
"Elliptical": "Elliptical",
"Flip H": "Flip H",
+10 -9
View File
@@ -10,18 +10,19 @@
"Clear": "Limpiar",
"Coronal": "Coronal",
"Crosshairs": "Punto de mira",
"Download": "Descargar",
"Ellipse": "Elipse",
"Elliptical": "Elíptico",
"Flip H": "Espejo H",
"Flip V": "Espejo V",
"Flip H": "Voltear H",
"Flip V": "Voltear V",
"Freehand": "Mano alzada",
"Invert": "Invertir",
"Invert": "Negativo",
"Layout": "$t(Common:Layout)",
"Length": "Longitud",
"Levels": "Niveles",
"Magnify": "Aumentar",
"Levels": "W/L",
"Magnify": "Lupa",
"Manual": "Manual",
"Measurements": "Mediciones",
"Measurements": "Medidas",
"More": "$t(Common:More)",
"Next": "$t(Common:Next)",
"Pan": "Mover",
@@ -32,11 +33,11 @@
"Rectangle": "Rectángulo",
"Reset": "$t(Common:Reset)",
"Reset to Defaults": "$t(Common:Reset) por defecto",
"Rotate Right": "Rotar ->",
"Rotate Right": "Girar ->",
"Sagittal": "Sagital",
"Save": "Guardar",
"Stack Scroll": "Scroll en pila",
"Stack Scroll": "Scroll",
"Stop": "$t(Common:Stop)",
"Themes": "Temas",
"Zoom": "Zoom"
"Zoom": "Ampliar"
}
+2 -2
View File
@@ -1,12 +1,12 @@
{
"Image": "Imagen",
"Layout": "Disposición",
"Layout": "Formato",
"Measurements": "Medidas",
"More": "Más",
"Next": "Siguiente",
"Play": "Play",
"Previous": "Anterior",
"Reset": "Resetear",
"Reset": "Restaurar",
"RowsPerPage": "filas por página",
"Series": "Secuencia",
"Show": "Mostrar",
+3 -2
View File
@@ -10,6 +10,7 @@
"Clear": "Limpar",
"Coronal": "Coronal",
"Crosshairs": "Localizador",
"Download": "Baixar",
"Ellipse": "Elipse",
"Elliptical": "Elíptico",
"Flip H": "Inverter H",
@@ -30,8 +31,8 @@
"Probe": "Prova",
"ROI Window": "Janela ROI",
"Rectangle": "Retângulo",
"Reset": "Resetar",
"Reset to Defaults": "Restaurar para o Padrão",
"Reset": "$t(Common:Reset)",
"Reset to Defaults": "$t(Common:Reset) para o Padrão",
"Rotate Right": "Girar à direita",
"Sagittal": "Sagital",
"Save": "Salvar",
+1 -1
View File
@@ -6,6 +6,6 @@
"Next": "Próximo",
"Play": "Play",
"Previous": "Anterior",
"Reset": "Reset",
"Reset": "Restaurar",
"Stop": "Stop"
}
@@ -1,6 +1,6 @@
{
"Cancel": "Cancelar",
"Reset to Defaults": "Restaurar Default",
"Reset to Defaults": "$t(Common:Reset) para Padrão",
"ResetDefaultMessage": "Preferências resetadas com sucesso. <br /> Você deve <strong>Salvar</strong> para que essa ação seja realizada.",
"Save": "Salvar",
"SaveMessage": "Preferências salvas",
+98
View File
@@ -3,6 +3,104 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.1.7](https://github.com/OHIF/Viewers/compare/@ohif/ui@1.1.6...@ohif/ui@1.1.7) (2019-12-20)
**Note:** Version bump only for package @ohif/ui
## [1.1.6](https://github.com/OHIF/Viewers/compare/@ohif/ui@1.1.5...@ohif/ui@1.1.6) (2019-12-20)
**Note:** Version bump only for package @ohif/ui
## [1.1.5](https://github.com/OHIF/Viewers/compare/@ohif/ui@1.1.4...@ohif/ui@1.1.5) (2019-12-19)
### Bug Fixes
* 🐛 Fix drag-n-drop of local files into OHIF ([#1319](https://github.com/OHIF/Viewers/issues/1319)) ([23305ce](https://github.com/OHIF/Viewers/commit/23305cec9c0f514e73a8dd17f984ffc87ad8d131)), closes [#1307](https://github.com/OHIF/Viewers/issues/1307)
## [1.1.4](https://github.com/OHIF/Viewers/compare/@ohif/ui@1.1.3...@ohif/ui@1.1.4) (2019-12-16)
**Note:** Version bump only for package @ohif/ui
## [1.1.3](https://github.com/OHIF/Viewers/compare/@ohif/ui@1.1.2...@ohif/ui@1.1.3) (2019-12-13)
### Bug Fixes
* allow empty values for dimensions ([#1295](https://github.com/OHIF/Viewers/issues/1295)) ([cd2da34](https://github.com/OHIF/Viewers/commit/cd2da349e5212cccdd8e65ffa3f7fdc6bad1057c))
## [1.1.2](https://github.com/OHIF/Viewers/compare/@ohif/ui@1.1.1...@ohif/ui@1.1.2) (2019-12-12)
### Bug Fixes
* translations ([#1234](https://github.com/OHIF/Viewers/issues/1234)) ([30b9e44](https://github.com/OHIF/Viewers/commit/30b9e4422073557287ef26a80b38eeb3f3fcff4c))
## [1.1.1](https://github.com/OHIF/Viewers/compare/@ohif/ui@1.1.0...@ohif/ui@1.1.1) (2019-12-11)
**Note:** Version bump only for package @ohif/ui
# [1.1.0](https://github.com/OHIF/Viewers/compare/@ohif/ui@1.0.1...@ohif/ui@1.1.0) (2019-12-11)
### Features
* 🎸 DICOM SR STOW on MeasurementAPI ([#954](https://github.com/OHIF/Viewers/issues/954)) ([ebe1af8](https://github.com/OHIF/Viewers/commit/ebe1af8d4f75d2483eba869655906d7829bd9666)), closes [#758](https://github.com/OHIF/Viewers/issues/758)
## [1.0.1](https://github.com/OHIF/Viewers/compare/@ohif/ui@1.0.0...@ohif/ui@1.0.1) (2019-12-09)
**Note:** Version bump only for package @ohif/ui
# [1.0.0](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.65.4...@ohif/ui@1.0.0) (2019-12-09)
* feat!: Ability to configure cornerstone tools via extension configuration (#1229) ([55a5806](https://github.com/OHIF/Viewers/commit/55a580659ecb74ca6433461d8f9a05c2a2b69533)), closes [#1229](https://github.com/OHIF/Viewers/issues/1229)
### BREAKING CHANGES
* modifies the exposed react <App /> components props. The contract for providing configuration for the app has changed. Please reference updated documentation for guidance.
## [0.65.4](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.65.3...@ohif/ui@0.65.4) (2019-12-07)
**Note:** Version bump only for package @ohif/ui
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/ui",
"version": "0.65.4",
"version": "1.1.7",
"description": "A set of React components for Medical Imaging Viewers",
"author": "OHIF Contributors",
"license": "MIT",
@@ -32,69 +32,52 @@ const ViewportDownloadForm = ({
maximumSize,
canvasClass,
}) => {
const [t] = useTranslation('ViewportDownloadForm');
const [t] = useTranslation('Buttons');
const [filename, setFilename] = useState(DEFAULT_FILENAME);
const [fileType, setFileType] = useState('jpg');
const [height, setHeight] = useState(defaultSize);
const [width, setWidth] = useState(defaultSize);
const [dimensions, setDimensions] = useState({
width: defaultSize,
height: defaultSize,
});
const [showAnnotations, setShowAnnotations] = useState(true);
const [keepAspect, setKeepAspect] = useState(true);
const [lastImage, setLastImage] = useState();
const [viewportElement, setViewportElement] = useState();
const [viewportElementHeight, setViewportElementHeight] = useState(
minimumSize
);
const [viewportElementWidth, setViewportElementWidth] = useState(minimumSize);
const [viewportElementDimensions, setViewportElementDimensions] = useState({
width: defaultSize,
height: defaultSize,
});
const [downloadCanvas, setDownloadCanvas] = useState({
ref: createRef(),
width: minimumSize,
height: minimumSize,
width: defaultSize,
height: defaultSize,
});
const [viewportPreview, setViewportPreview] = useState({
src: null,
width: minimumSize,
height: minimumSize,
width: defaultSize,
height: defaultSize,
});
// Cornerstone's `enable/disable`
useEffect(() => {
enableViewport(viewportElement);
return () => {
disableViewport(viewportElement);
setHeight(defaultSize);
setWidth(defaultSize);
};
}, [defaultSize, disableViewport, enableViewport, viewportElement]);
}, [disableViewport, enableViewport, viewportElement]);
useEffect(() => {
const { width, height } = viewportElementDimensions;
const validSize = value => (value >= minimumSize ? value : minimumSize);
const loadAndUpdateViewports = async () => {
const {
image,
width: scaledWidth,
height: scaledHeight,
} = await loadImage(activeViewport, viewportElement, width, height);
setLastImage(image);
await loadImage(activeViewport, viewportElement, width, height);
toggleAnnotations(showAnnotations, viewportElement);
setViewportElementHeight(validSize(scaledHeight));
setViewportElementWidth(validSize(scaledWidth));
setDownloadCanvas(state => ({
...state,
height: validSize(scaledHeight),
width: validSize(scaledWidth),
}));
const {
dataUrl,
width: viewportElementWidth,
@@ -118,8 +101,6 @@ const ViewportDownloadForm = ({
activeViewport,
viewportElement,
showAnnotations,
height,
width,
loadImage,
toggleAnnotations,
updateViewportPreview,
@@ -127,54 +108,41 @@ const ViewportDownloadForm = ({
downloadCanvas.ref,
minimumSize,
maximumSize,
viewportElementDimensions,
]);
const onHeightChange = event => {
const newHeight = Math.min(event.target.value, maximumSize);
setHeight(newHeight);
/**
* @param {object} event - Input change event
* @param {string} dimension - "height" | "width"
*/
const onDimensionsChange = (event, dimension) => {
const sanitizedTargetValue = event.target.value.replace(/\D/, '');
const isEmpty = sanitizedTargetValue === '';
const updatedDimension = isEmpty
? ''
: Math.min(sanitizedTargetValue, maximumSize);
setViewportElementHeight(newHeight);
setDownloadCanvas(state => ({
...state,
height: newHeight,
}));
if (keepAspect) {
const multiplier = newHeight / lastImage.height;
const newWidth = Math.round(lastImage.width * multiplier);
setWidth(newWidth);
setViewportElementWidth(newWidth);
setDownloadCanvas(state => ({
...state,
width: newWidth,
}));
if (updatedDimension === dimensions.width) {
return;
}
};
const onWidthChange = event => {
const newWidth = Math.min(event.target.value, maximumSize);
setWidth(newWidth);
setViewportElementWidth(newWidth);
setDownloadCanvas(state => ({
...state,
width: newWidth,
}));
if (keepAspect) {
const multiplier = newWidth / lastImage.width;
const newHeight = Math.round(lastImage.height * multiplier);
setHeight(newHeight);
setViewportElementHeight(newHeight);
// In current code, keepAspect is always `true`
// And we always start w/ a square width/height
setDimensions({
width: updatedDimension,
height: updatedDimension,
});
// Only update if value is non-empty
if (!isEmpty) {
setViewportElementDimensions({
height: updatedDimension,
width: updatedDimension,
});
setDownloadCanvas(state => ({
...state,
height: newHeight,
height: updatedDimension,
width: updatedDimension,
}));
}
};
@@ -196,26 +164,22 @@ const ViewportDownloadForm = ({
)}
</div>
<div className="file-info-container">
<div className="file-info-container" data-cy="file-info-container">
<div className="col">
<div className="width">
<TextInput
type="number"
min={minimumSize}
max={maximumSize}
value={width}
data-cy="image-width"
value={dimensions.width}
label={t('Image width (px)')}
onChange={onWidthChange}
onChange={evt => onDimensionsChange(evt, 'height')}
/>
</div>
<div className="height">
<TextInput
type="number"
min={minimumSize}
max={maximumSize}
value={height}
data-cy="image-height"
value={dimensions.height}
label={t('Image height (px)')}
onChange={onHeightChange}
onChange={evt => onDimensionsChange(evt, 'width')}
/>
</div>
</div>
@@ -224,6 +188,7 @@ const ViewportDownloadForm = ({
<div className="file-name">
<TextInput
type="text"
data-cy="file-name"
value={filename}
onChange={event => setFilename(event.target.value)}
label={t('File name')}
@@ -233,6 +198,7 @@ const ViewportDownloadForm = ({
<div className="file-type">
<Select
value={fileType}
data-cy="file-type"
onChange={event => setFileType(event.target.value)}
options={FILE_TYPE_OPTIONS}
label={t('File type')}
@@ -245,6 +211,7 @@ const ViewportDownloadForm = ({
<label htmlFor="show-annotations" className="form-check-label">
<input
id="show-annotations"
data-cy="show-annotations"
type="checkbox"
className="form-check-input"
checked={showAnnotations}
@@ -258,8 +225,8 @@ const ViewportDownloadForm = ({
<div
style={{
height: viewportElementHeight,
width: viewportElementWidth,
height: viewportElementDimensions.height,
width: viewportElementDimensions.width,
position: 'absolute',
left: '9999px',
}}
@@ -278,23 +245,33 @@ const ViewportDownloadForm = ({
></canvas>
</div>
<div className="preview">
<div className="preview" data-cy="image-preview">
<h4> {t('Image Preview')}</h4>
<img
className="viewport-preview"
src={viewportPreview.src}
alt="Viewport Preview"
data-cy="viewport-preview-img"
/>
</div>
<div className="actions">
<div className="action-cancel">
<button type="button" className="btn btn-danger" onClick={onClose}>
<button
type="button"
data-cy="cancel-btn"
className="btn btn-danger"
onClick={onClose}
>
{t('Cancel')}
</button>
</div>
<div className="action-save">
<button onClick={downloadImage} className="btn btn-primary">
<button
onClick={downloadImage}
className="btn btn-primary"
data-cy="download-btn"
>
{t('Download')}
</button>
</div>
@@ -312,6 +289,7 @@ ViewportDownloadForm.propTypes = {
toggleAnnotations: PropTypes.func.isRequired,
loadImage: PropTypes.func.isRequired,
downloadBlob: PropTypes.func.isRequired,
/** A default width & height, between the minimum and maximum size */
defaultSize: PropTypes.number.isRequired,
minimumSize: PropTypes.number.isRequired,
maximumSize: PropTypes.number.isRequired,
@@ -1,20 +1,19 @@
.ToolContextMenu {
position: absolute;
.ContextMenu {
position: relative;
background-color: white;
border: 1px solid white;
border-radius: 5px;
z-index: 1000;
display: block;
width: 170px;
}
.ToolContextMenu > ul {
.ContextMenu>ul {
list-style-type: none;
padding-left: 0;
margin: 0;
}
.ToolContextMenu > ul > li > button {
.ContextMenu>ul>li>button {
padding: 10px;
font-size: 14px;
border: none;
@@ -25,10 +24,10 @@
background: none;
}
.ToolContextMenu > ul > li > button:hover {
.ContextMenu>ul>li>button:hover {
color: #16202b;
}
.ToolContextMenu > ul > li > button:active {
.ContextMenu>ul>li>button:active {
color: #79f9fe;
}
@@ -0,0 +1,27 @@
import React from 'react';
import PropTypes from 'prop-types';
import './ContextMenu.css';
const ContextMenu = ({ items, onClick }) => {
return (
<div className="ContextMenu">
<ul>
{items.map((item, index) => (
<li key={index}>
<button className="form-action" onClick={() => onClick(item)}>
<span key={index}>{item.label}</span>
</button>
</li>
))}
</ul>
</div>
);
};
ContextMenu.propTypes = {
items: PropTypes.array.isRequired,
onClick: PropTypes.func.isRequired,
};
export default ContextMenu;
@@ -0,0 +1 @@
export { default as ContextMenu } from './ContextMenu.js';
+2
View File
@@ -17,6 +17,7 @@ import { RoundedButtonGroup } from './roundedButtonGroup';
import { SelectTree } from './selectTree';
import { SimpleDialog } from './simpleDialog';
import { OHIFModal } from './ohifModal';
import { ContextMenu } from './contextMenu';
import {
PageToolbar,
StudyList,
@@ -27,6 +28,7 @@ import { ToolbarSection } from './toolbarSection';
import { Tooltip } from './tooltip';
export {
ContextMenu,
Checkbox,
CineDialog,
ViewportDownloadForm,
@@ -22,8 +22,9 @@ class MeasurementTable extends Component {
onDeleteClick: PropTypes.func,
onEditDescriptionClick: PropTypes.func,
selectedMeasurementNumber: PropTypes.number,
overwallWarnings: PropTypes.object,
t: PropTypes.func,
saveFunction: PropTypes.func,
onSaveComplete: PropTypes.func,
};
static defaultProps = {
@@ -38,8 +39,9 @@ class MeasurementTable extends Component {
};
render() {
const hasOverallWarnings =
this.props.overallWarnings.warningList.length > 0;
const { overallWarnings, saveFunction, t } = this.props;
const hasOverallWarnings = overallWarnings.warningList.length > 0;
return (
<div className="measurementTable">
<div className="measurementTableHeader">
@@ -55,7 +57,7 @@ class MeasurementTable extends Component {
style={{}}
>
<div className="warningTitle">
{this.props.t('Criteria nonconformities')}
{t('Criteria nonconformities')}
</div>
<div className="warningContent">
{this.getWarningContent()}
@@ -75,10 +77,46 @@ class MeasurementTable extends Component {
<ScrollableArea>
<div>{this.getMeasurementsGroups()}</div>
</ScrollableArea>
<div className="measurementTableFooter">
{saveFunction && (
<button
onClick={this.saveFunction}
className="saveBtn"
data-cy="save-measurements-btn"
>
<Icon name="save" width="14px" height="14px" />
Save measurements
</button>
)}
</div>
</div>
);
}
saveFunction = async event => {
const { saveFunction, onSaveComplete } = this.props;
if (saveFunction) {
try {
const result = await saveFunction();
if (onSaveComplete) {
onSaveComplete({
title: 'STOW SR',
message: result.message,
type: 'success',
});
}
} catch (error) {
if (onSaveComplete) {
onSaveComplete({
title: 'STOW SR',
message: error.message,
type: 'error',
});
}
}
}
};
getMeasurementsGroups = () => {
return this.props.measurementCollection.map((measureGroup, index) => {
return (
@@ -157,7 +195,7 @@ class MeasurementTable extends Component {
};
getWarningContent = () => {
const { warningList = '' } = this.props.overwallWarnings;
const { warningList = '' } = this.props.overallWarnings;
if (Array.isArray(warningList)) {
const listedWarnings = warningList.map((warn, index) => {
@@ -1,92 +1,117 @@
.measurementTable
background-color: var(--primary-background-color)
height: 100%;
width: 100%;
display: flex;
flex-direction: column;
height: 100%
width: 100%
display: flex
flex-direction: column
.maxMeasurements
border-radius: 3px;
color: var(--primary-background-color);
font-size: 12px;
font-weight: 500;
height: 19px;
line-height: 17px;
margin-top: 22px;
margin-right: 15px;
padding: 2px 6px 0;
text-transform: uppercase;
transition: all 0.3s ease;
background-color: var(--default-color);
border-radius: 3px
color: var(--primary-background-color)
font-size: 12px
font-weight: 500
height: 19px
line-height: 17px
margin-top: 22px
margin-right: 15px
padding: 2px 6px 0
text-transform: uppercase
transition: all 0.3s ease
background-color: var(--default-color)
.measurementTableHeader
padding-left: 45px;
padding-left: 45px
.measurementTableHeaderItem
display: inline-block;
margin-top: 9px;
margin-left: 9px;
padding-left: 9px;
width: 90px;
font-size: 14px;
line-height: 20px;
border-left: 1px solid var(--text-secondary-color);
color: var(--text-primary-color);
display: inline-block
margin-top: 9px
margin-left: 9px
padding-left: 9px
width: 90px
font-size: 14px
line-height: 20px
border-left: 1px solid var(--text-secondary-color)
color: var(--text-primary-color)
.timepointLabel
color: var(--text-secondary-color);
font-size: 12px;
color: var(--text-secondary-color)
font-size: 12px
.warning-status
display: block;
position: absolute;
width: 50px;
height: 40px;
padding-left: 7px;
left: 15px;
top: 7px;
display: block
position: absolute
width: 50px
height: 40px
padding-left: 7px
left: 15px
top: 7px
.warning-border
display: block;
border: 2px solid #e29e4a;
border-radius: 36px;
padding: 6px 7px;
width: 40px;
height: 40px;
display: block
border: 2px solid #e29e4a
border-radius: 36px
padding: 6px 7px
width: 40px
height: 40px
svg
color: #e29e4a;
width: 22px;
height: 20px;
pointer-events: inherit;
color: #e29e4a
width: 22px
height: 20px
pointer-events: inherit
.measurementTableFooter
padding: 10px
color: #fff
text-align: center
z-index: 1
.saveBtn
border: 1px solid var(--ui-border-color-active)
border-radius: 25px
background-color: var(--active-color)
color: var(--ui-gray-dark)
line-height: 25px
padding: 0 20px
outline: none
cursor: pointer
&:hover, &:active
background-color: var(--ui-sky-blue)
svg
margin-right: 4px
position: relative
top: 2px
.tooltip-warning
&.in
opacity 1;
opacity 1
.tooltip-inner
padding: 0
.warningTitle
text-align: left
background-color: var(--ui-gray-darkest);
padding: 7px 15px;
border-top-left-radius: 7px;
border-top-right-radius: 7px;
background-color: var(--ui-gray-darkest)
padding: 7px 15px
border-top-left-radius: 7px
border-top-right-radius: 7px
.warningContent
text-align center
background-color: var(--ui-gray);
border-bottom-left-radius: 7px;
border-bottom-right-radius: 7px;
background-color: var(--ui-gray)
border-bottom-left-radius: 7px
border-bottom-right-radius: 7px
ol
margin-top: auto;
padding-inline-start: 20px;
padding-top: 10px;
padding-bottom: 10px;
padding-left: 25px;
text-align: left;
margin-top: auto
padding-inline-start: 20px
padding-top: 10px
padding-bottom: 10px
padding-left: 25px
text-align: left
li
padding-right: 10px;
padding-right: 10px
@@ -29,7 +29,7 @@ const OHIFModal = ({
const renderHeader = () => {
return (
title && (
<div className="OHIFModal__header">
<div className="OHIFModal__header" data-cy="modal-header">
<h4>{title}</h4>
{closeButton && (
<button data-cy="close-button" onClick={onClose}>
@@ -44,6 +44,7 @@ const OHIFModal = ({
return (
<Modal
className={classNames('OHIFModal', className)}
data-cy="modal"
shouldCloseOnEsc={shouldCloseOnEsc}
isOpen={isOpen}
title={title}
@@ -51,7 +52,9 @@ const OHIFModal = ({
>
<>
{renderHeader()}
<div className="OHIFModal__content">{children}</div>
<div className="OHIFModal__content" data-cy="modal-content">
{children}
</div>
</>
</Modal>
);
@@ -6,9 +6,7 @@
position: relative
.simpleDialog
position: fixed;
top: 0px;
left: 0px;
position: relative;
z-index: 1000;
border: 0;
border-radius: 6px;
@@ -43,6 +43,7 @@ const renderTabsHeader = (tabs, activeTabIndex, onHeaderChanged) => {
? tabs.map((tab, index) => {
const { name, hidden = false } = tab;
const cypressSelectorId = name.toLowerCase();
const tabClass =
index === activeTabIndex ? 'nav-link active' : 'nav-link';
return !hidden ? (
@@ -52,6 +53,7 @@ const renderTabsHeader = (tabs, activeTabIndex, onHeaderChanged) => {
onHeaderChanged(index);
}}
className={tabClass}
data-cy={cypressSelectorId}
>
<button>{name}</button>
</li>
@@ -20,7 +20,30 @@ export const useDialog = () => useContext(DialogContext);
const DialogProvider = ({ children, service }) => {
const [isDragging, setIsDragging] = useState(false);
const [dialogs, setDialogs] = useState([]);
const [lastDialogId, setLastDialogId] = useState(null);
const [lastDialogPosition, setLastDialogPosition] = useState(null);
const [centerPositions, setCenterPositions] = useState([]);
useEffect(() => {
setCenterPositions(
dialogs.map(dialog => ({
id: dialog.id,
...getCenterPosition(dialog.id),
}))
);
}, [dialogs]);
const getCenterPosition = id => {
const root = document.querySelector('#root');
const centerX = root.offsetLeft + root.offsetWidth / 2;
const centerY = root.offsetTop + root.offsetHeight / 2;
const item = document.querySelector(`#draggableItem-${id}`);
const itemBounds = item.getBoundingClientRect();
return {
x: centerX - itemBounds.width / 2,
y: centerY - itemBounds.height / 2,
};
};
/**
* Sets the implementation of a dialog service that can be used by extensions.
@@ -42,13 +65,16 @@ const DialogProvider = ({ children, service }) => {
* @property {Object} contentProps The dialog content props.
* @property {boolean} isDraggable Controls if dialog content is draggable or not.
* @property {boolean} showOverlay Controls dialog overlay.
* @property {boolean} centralize Center the dialog on the screen.
* @property {boolean} preservePosition Use last position instead of default.
* @property {ElementPosition} defaultPosition Specifies the `x` and `y` that the dragged item should start at.
* @property {ElementPosition} position If this property is present, the item becomes 'controlled' and is not responsive to user input.
* @property {Function} onStart Called when dragging starts. If `false` is returned any handler, the action will cancel.
* @property {Function} onStop Called when dragging stops.
* @property {Function} onDrag Called while dragging.
*/
useEffect(() => _bringToFront(lastDialogId), [_bringToFront, lastDialogId]);
/**
* Creates a new dialog and return its id.
*
@@ -64,6 +90,7 @@ const DialogProvider = ({ children, service }) => {
}
setDialogs(dialogs => [...dialogs, { ...props, id: dialogId }]);
setLastDialogId(dialogId);
return dialogId;
}, []);
@@ -75,9 +102,11 @@ const DialogProvider = ({ children, service }) => {
* @property {string} props.id The dialog id.
* @returns void
*/
const dismiss = useCallback(({ id }) => {
setDialogs(dialogs => dialogs.filter(dialog => dialog.id !== id));
}, []);
const dismiss = useCallback(
({ id }) =>
setDialogs(dialogs => dialogs.filter(dialog => dialog.id !== id)),
[]
);
/**
* Dismisses all dialogs.
@@ -101,14 +130,14 @@ const DialogProvider = ({ children, service }) => {
* @param {string} id The dialog id.
* @returns void
*/
const _bringToFront = id => {
const _bringToFront = useCallback(id => {
setDialogs(dialogs => {
const topDialog = dialogs.find(dialog => dialog.id === id);
return topDialog
? [...dialogs.filter(dialog => dialog.id !== id), topDialog]
: [];
: dialogs;
});
};
}, []);
const renderDialogs = () =>
dialogs.map(dialog => {
@@ -116,20 +145,28 @@ const DialogProvider = ({ children, service }) => {
id,
content: DialogContent,
contentProps,
position,
defaultPosition,
centralize = false,
preservePosition = true,
isDraggable = true,
onStart,
onStop,
onDrag,
showOverlay,
} = dialog;
return (
let position =
(preservePosition && lastDialogPosition) || defaultPosition;
if (centralize) {
position = centerPositions.find(position => position.id === id);
}
const dragableItem = () => (
<Draggable
key={id}
disabled={!isDraggable}
position={position}
defaultPosition={lastDialogPosition || defaultPosition}
defaultPosition={position}
bounds="parent"
onStart={event => {
const e = event || window.event;
@@ -169,7 +206,11 @@ const DialogProvider = ({ children, service }) => {
>
<div
id={`draggableItem-${id}`}
className={classNames('DraggableItem', isDragging && 'dragging')}
className={classNames(
'DraggableItem',
isDragging && 'dragging',
isDraggable && 'draggable'
)}
style={{ zIndex: '999', position: 'absolute' }}
onClick={() => _bringToFront(id)}
>
@@ -177,6 +218,16 @@ const DialogProvider = ({ children, service }) => {
</div>
</Draggable>
);
return (
showOverlay ? (
<div className="Overlay" key={id}>
{dragableItem()}
</div>
) : (
dragableItem()
)
);
});
/**
@@ -198,13 +249,11 @@ const DialogProvider = ({ children, service }) => {
return (
<DialogContext.Provider value={{ create, dismiss, dismissAll, isEmpty }}>
<div className="DraggableArea">
{dialogs.some(dialog => dialog.showOverlay) ? (
<div className="Overlay active">{renderDialogs()}</div>
) : (
renderDialogs()
)}
</div>
{!isEmpty() &&
<div className="DraggableArea">
{renderDialogs()}
</div>
}
{children}
</DialogContext.Provider>
);
@@ -1,22 +1,21 @@
.DraggableItem
.DraggableItem.draggable
div
cursor: grab !important
.DraggableItem.dragging
.DraggableItem.draggable.dragging
div
cursor: grabbing !important
.DraggableArea, .Overlay
.DraggableArea
width: 100%
height: 100%
position: absolute
.Overlay.active
position: fixed
z-index: 999
left: 0
top: 0
width: 100%
height: 100%
overflow: auto
background: rgba(0,0,0,.1)
.Overlay
position: fixed
z-index: 999
left: 0
top: 0
width: 100%
height: 100%
overflow: auto
background: rgba(0,0,0,.1)
+2
View File
@@ -62,6 +62,7 @@ import powerOff from './icons/power-off.svg';
import reset from './icons/reset.svg';
import rotate from './icons/rotate.svg';
import rotateRight from './icons/rotate-right.svg';
import saveRegular from './icons/save-regular.svg';
import search from './icons/search.svg';
import searchPlus from './icons/search-plus.svg';
import softTissue from './icons/soft-tissue.svg';
@@ -164,6 +165,7 @@ const ICONS = {
'soft-tissue': softTissue,
lung,
liver,
save: saveRegular,
};
/**
@@ -0,0 +1,11 @@
<svg
aria-hidden="true"
focusable="false"
data-icon="save"
role="img" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 448 512">
<path
fill="currentColor"
d="M433.941 129.941l-83.882-83.882A48 48 0 0 0 316.118 32H48C21.49 32 0 53.49 0 80v352c0 26.51 21.49 48 48 48h352c26.51 0 48-21.49 48-48V163.882a48 48 0 0 0-14.059-33.941zM272 80v80H144V80h128zm122 352H54a6 6 0 0 1-6-6V86a6 6 0 0 1 6-6h42v104c0 13.255 10.745 24 24 24h176c13.255 0 24-10.745 24-24V83.882l78.243 78.243a6 6 0 0 1 1.757 4.243V426a6 6 0 0 1-6 6zM224 232c-48.523 0-88 39.477-88 88s39.477 88 88 88 88-39.477 88-88-39.477-88-88-88zm0 128c-22.056 0-40-17.944-40-40s17.944-40 40-40 40 17.944 40 40-17.944 40-40 40z">
</path>
</svg>

After

Width:  |  Height:  |  Size: 738 B

+2
View File
@@ -1,4 +1,5 @@
import {
ContextMenu,
Checkbox,
CineDialog,
ViewportDownloadForm,
@@ -72,6 +73,7 @@ export {
TextArea,
TextInput,
CineDialog,
ContextMenu,
ViewportDownloadForm,
ExpandableToolMenu,
Icon,
+7 -1
View File
@@ -23,8 +23,14 @@ export function ToolbarButton(props) {
}
};
const cypressSelectorId = props.label.toLowerCase();
return (
<div className={className} onClick={handleClick}>
<div
className={className}
onClick={handleClick}
data-cy={cypressSelectorId}
>
{iconProps && <Icon {...iconProps} />}
<div className="toolbar-button-label">
{t(label)}
+224
View File
@@ -3,6 +3,230 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [3.3.4](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.3.3...@ohif/viewer@3.3.4) (2019-12-30)
**Note:** Version bump only for package @ohif/viewer
## [3.3.3](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.3.2...@ohif/viewer@3.3.3) (2019-12-20)
### Bug Fixes
* 🐛 1241: Make Plugin switch part of ToolbarModule ([#1322](https://github.com/OHIF/Viewers/issues/1322)) ([6540e36](https://github.com/OHIF/Viewers/commit/6540e36818944ac2eccc696186366ae495b33a04)), closes [#1241](https://github.com/OHIF/Viewers/issues/1241)
## [3.3.2](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.3.1...@ohif/viewer@3.3.2) (2019-12-20)
**Note:** Version bump only for package @ohif/viewer
## [3.3.1](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.3.0...@ohif/viewer@3.3.1) (2019-12-20)
**Note:** Version bump only for package @ohif/viewer
# [3.3.0](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.2.2...@ohif/viewer@3.3.0) (2019-12-20)
### Features
* 🎸 Configuration so viewer tools can nix handles ([#1304](https://github.com/OHIF/Viewers/issues/1304)) ([63594d3](https://github.com/OHIF/Viewers/commit/63594d36b0bdba59f0901095aed70b75fb05172d)), closes [#1223](https://github.com/OHIF/Viewers/issues/1223)
## [3.2.2](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.2.1...@ohif/viewer@3.2.2) (2019-12-19)
### Bug Fixes
* 🐛 Fix drag-n-drop of local files into OHIF ([#1319](https://github.com/OHIF/Viewers/issues/1319)) ([23305ce](https://github.com/OHIF/Viewers/commit/23305cec9c0f514e73a8dd17f984ffc87ad8d131)), closes [#1307](https://github.com/OHIF/Viewers/issues/1307)
## [3.2.1](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.2.0...@ohif/viewer@3.2.1) (2019-12-18)
**Note:** Version bump only for package @ohif/viewer
# [3.2.0](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.1.12...@ohif/viewer@3.2.0) (2019-12-16)
### Features
* 🎸 Expose extension config to modules ([#1279](https://github.com/OHIF/Viewers/issues/1279)) ([4ea239a](https://github.com/OHIF/Viewers/commit/4ea239a9535ef297e23387c186e537ab273744ea)), closes [#1268](https://github.com/OHIF/Viewers/issues/1268)
## [3.1.12](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.1.11...@ohif/viewer@3.1.12) (2019-12-16)
### Bug Fixes
* 🐛 Dismiss all dialogs if leaving viewer route [#1242](https://github.com/OHIF/Viewers/issues/1242) ([#1301](https://github.com/OHIF/Viewers/issues/1301)) ([5c3d8b3](https://github.com/OHIF/Viewers/commit/5c3d8b37b6f723fbd8edcc447c37984e7eee8d40))
## [3.1.11](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.1.10...@ohif/viewer@3.1.11) (2019-12-16)
**Note:** Version bump only for package @ohif/viewer
## [3.1.10](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.1.9...@ohif/viewer@3.1.10) (2019-12-16)
**Note:** Version bump only for package @ohif/viewer
## [3.1.9](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.1.8...@ohif/viewer@3.1.9) (2019-12-16)
**Note:** Version bump only for package @ohif/viewer
## [3.1.8](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.1.7...@ohif/viewer@3.1.8) (2019-12-16)
**Note:** Version bump only for package @ohif/viewer
## [3.1.7](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.1.6...@ohif/viewer@3.1.7) (2019-12-13)
**Note:** Version bump only for package @ohif/viewer
## [3.1.6](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.1.5...@ohif/viewer@3.1.6) (2019-12-13)
**Note:** Version bump only for package @ohif/viewer
## [3.1.5](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.1.4...@ohif/viewer@3.1.5) (2019-12-13)
**Note:** Version bump only for package @ohif/viewer
## [3.1.4](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.1.3...@ohif/viewer@3.1.4) (2019-12-13)
**Note:** Version bump only for package @ohif/viewer
## [3.1.3](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.1.2...@ohif/viewer@3.1.3) (2019-12-12)
**Note:** Version bump only for package @ohif/viewer
## [3.1.2](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.1.1...@ohif/viewer@3.1.2) (2019-12-12)
**Note:** Version bump only for package @ohif/viewer
## [3.1.1](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.1.0...@ohif/viewer@3.1.1) (2019-12-11)
**Note:** Version bump only for package @ohif/viewer
# [3.1.0](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.0.3...@ohif/viewer@3.1.0) (2019-12-11)
### Features
* 🎸 DICOM SR STOW on MeasurementAPI ([#954](https://github.com/OHIF/Viewers/issues/954)) ([ebe1af8](https://github.com/OHIF/Viewers/commit/ebe1af8d4f75d2483eba869655906d7829bd9666)), closes [#758](https://github.com/OHIF/Viewers/issues/758)
## [3.0.3](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.0.2...@ohif/viewer@3.0.3) (2019-12-11)
**Note:** Version bump only for package @ohif/viewer
## [3.0.2](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.0.1...@ohif/viewer@3.0.2) (2019-12-11)
**Note:** Version bump only for package @ohif/viewer
## [3.0.1](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.0.0...@ohif/viewer@3.0.1) (2019-12-09)
**Note:** Version bump only for package @ohif/viewer
# [3.0.0](https://github.com/OHIF/Viewers/compare/@ohif/viewer@2.11.8...@ohif/viewer@3.0.0) (2019-12-09)
* feat!: Ability to configure cornerstone tools via extension configuration (#1229) ([55a5806](https://github.com/OHIF/Viewers/commit/55a580659ecb74ca6433461d8f9a05c2a2b69533)), closes [#1229](https://github.com/OHIF/Viewers/issues/1229)
### BREAKING CHANGES
* modifies the exposed react <App /> components props. The contract for providing configuration for the app has changed. Please reference updated documentation for guidance.
## [2.11.8](https://github.com/OHIF/Viewers/compare/@ohif/viewer@2.11.7...@ohif/viewer@2.11.8) (2019-12-07)
**Note:** Version bump only for package @ohif/viewer
Loaded 100 of 155 files, more files were not shown because too many files have changed in this diff. Show more