Compare commits

..
Author SHA1 Message Date
ohif-bot adc8d9ea0f chore(release): publish [skip ci]
- @ohif/extension-cornerstone@1.6.0
 - @ohif/extension-vtk@0.53.11
 - @ohif/core@1.12.0
 - @ohif/ui@0.64.0
 - @ohif/viewer@2.10.0
2019-11-19 19:19:32 +00:00
Igor Octaviano f65639c2b0 feat: New dialog service (#1202)
* Add dialog provider boilerplate and improve provider imports

* Add react-draggable dependency

* Add dialog service boilerplate

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

* Update cinedialog to use new dialog provider

* Add multiple dialogs and refactor provider (dialogs)

* Set app root (modal)

* Add modal zindex bigger than tooltip

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

* Use guid util to generate dialog ids

* Explicit props in dialog provider

* Improve jsdocs

* Fix tests broken

* Fix boundaries calculation

* Remember last dialog position

* Update providers location

* Add scroll to modal

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

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

* fix: Set SR viewport as active by interaction

* quick fix

* (eslint) add "before" as global variables

* add data-cy

* add data-cy

* create E2E test

* (E2E) create custom command to set layout size

* remove .only e2e

* remove throttle for onScroll

* feat: 🎸 Code review in progress

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

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

* feat: 🎸 Code review missing changes from previous commit

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

* feat: 🎸 Code review. Add more jsdoc info

* feat: 🎸 Code review. Prettify changed code

* feat: 🎸 Instead of filtering do promote. WIP

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

* feat: 🎸 Changing component to functional component

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

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

* feat: 🎸 Add cancelable promises to cut async methods

* feat: 🎸 Missing changes from previous merge process

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

* remove unused values

* fix: search results w/ valid date returning empty

* translations from locize

* styling select

* revert studyListDateFilterNumDays
2019-11-18 16:30:47 -05:00
Danny Brown 6085b5f733 docs: add ohif urls to docs
docs: add ohif urls to docs
2019-11-18 11:33:48 -05:00
dannyrb ffe12789dd docs: add ohif urls to docs 2019-11-18 11:32:13 -05:00
Danny Brown 0b68429dfb Merge pull request #1200 from dannyrb/ci/complete-promoted-deploy
ci: fix typo
2019-11-18 11:07:50 -05:00
dannyrb b747ea5144 ci: fix typo 2019-11-18 11:06:57 -05:00
Danny Brown caadf089df Merge pull request #1199 from dannyrb/ci/complete-promoted-deploy
ci: Redirect site traffic to index.html if file is not resolved
2019-11-18 11:00:11 -05:00
Danny Brown e0fc69c181 Merge branch 'master' into ci/complete-promoted-deploy 2019-11-18 11:00:03 -05:00
dannyrb 2b9765304f ci: Redirect site traffic to index.html if file is not resolved 2019-11-18 10:57:21 -05:00
ohif-bot 57730483e9 chore(release): publish [skip ci]
- @ohif/extension-vtk@0.53.8
 - @ohif/core@1.10.0
 - @ohif/ui@0.62.3
 - @ohif/viewer@2.8.4
2019-11-15 20:04:45 +00:00
Danny Brown 731b64e321 feat: new modal service
feat: new modal service
2019-11-15 15:03:20 -05:00
Danny Brown e97a09b0f4 Merge branch 'master' into feat/new-modal-service 2019-11-15 14:52:37 -05:00
Danny Brown f63d8a73d8 feat: Inject into Extension Modules / improve tests
[UI Services to Extensions] Inject into Extension Modules / improve tests
2019-11-15 14:47:40 -05:00
Igor add3416e11 Fix key warning of snackbar elements 2019-11-15 09:54:14 -03:00
Igor 136969bf18 Add servicesManager test and registerServices method 2019-11-15 09:27:42 -03:00
Igor e3e039ad47 Pass services to each module, improve tests 2019-11-14 19:29:40 -03:00
84 changed files with 2169 additions and 488 deletions

No files matched your search

+6
View File
@@ -186,6 +186,8 @@ jobs:
- attach_workspace:
at: ~/repo
- run: cd .netlify && npm install
- run:
cp .netlify/deploy-workflow/_redirects platform/viewer/dist/_redirects
- run: cd .netlify && npm run deploy
DEPLOY_TO_STAGING:
@@ -199,6 +201,8 @@ jobs:
- attach_workspace:
at: ~/repo
- run: cd .netlify && npm install
- run:
cp .netlify/deploy-workflow/_redirects platform/viewer/dist/_redirects
- run: cd .netlify && npm run deploy
DEPLOY_TO_PRODUCTION:
@@ -212,6 +216,8 @@ jobs:
- attach_workspace:
at: ~/repo
- run: cd .netlify && npm install
- run:
cp .netlify/deploy-workflow/_redirects platform/viewer/dist/_redirects
- run: cd .netlify && npm run deploy
###
+1 -1
View File
@@ -2,7 +2,7 @@
name: "\U0001F41B Bug report"
about: Create a report to help us improve
title: ''
labels: 'Community: Report :bug:'
labels: 'Community: Report :bug:, Awaiting Reproduction, Triage :white_flag:'
assignees: ''
---
+1 -1
View File
@@ -2,7 +2,7 @@
name: "\U0001F680 Feature request"
about: Suggest an idea for this project
title: ''
labels: 'Community: Request :hand:'
labels: 'Community: Request :hand:, Triage :white_flag:'
assignees: ''
---
@@ -2,7 +2,7 @@
name: "\U0001F917 Support Question"
about: "I have a question \U0001F4AC"
title: ''
labels: 'Community: Question :question:'
labels: 'Community: Question :question:, Triage :white_flag:'
assignees: ''
---
+1 -1
View File
@@ -15,7 +15,7 @@ exemptLabels:
- 'Task: Refactor 🛠'
- 'Task: Tests :microscope:'
- 'PR: Awaiting Review 👀'
- 'Triage 🏳️'
- 'Triage :white_flag:'
- 'Announcement 🎉'
# Label to use when marking an issue as stale
staleLabel: 'Stale :baguette_bread:'
+5
View File
@@ -0,0 +1,5 @@
# Specific to our non-deploy-preview deploys
# Confgure redirects using netlify.toml
# PWA Redirect
/* /index.html 200
+1
View File
@@ -1,5 +1,6 @@
# OHIF Viewers
- [Our Process](our-process.md)
- Essentials
- [Getting Started](essentials/getting-started.md)
- [Installation](essentials/installation.md)
+9 -6
View File
@@ -63,11 +63,11 @@ promote the build to `STAGING` and `PRODUCTION` environments.
<div><i>Workflow diagram for WORKFLOW_DEPLOY</i></div>
</div>
| Environment | Description | URL |
| ----------- | ---------------------------------------------------------------------------------- | -------------------------- |
| Development | Always reflects latest changes on `master` branch. | [Netlify][netlify-dev] |
| Staging | For manual testing before promotion to prod. Keeps development workflow unblocked. | [Netlify][netlify-staging] |
| Production | Stable, tested, updated less frequently. | [Netlify][netlify-prod] |
| Environment | Description | URL |
| ----------- | ---------------------------------------------------------------------------------- | --------------------------------------------- |
| Development | Always reflects latest changes on `master` branch. | [Netlify][netlify-dev] / [OHIF][ohif-dev] |
| Staging | For manual testing before promotion to prod. Keeps development workflow unblocked. | [Netlify][netlify-stage] / [OHIF][ohif-stage] |
| Production | Stable, tested, updated less frequently. | [Netlify][netlify-prod] / [OHIF][ohif-prod] |
### Workflow: RELEASE
@@ -101,6 +101,9 @@ _Not yet implemented_
[deploy-previews]: https://www.netlify.com/blog/2016/07/20/introducing-deploy-previews-in-netlify/
[circleci-workflows]: https://circleci.com/docs/2.0/workflows/
[netlify-dev]: https://ohif-dev.netlify.com
[netlify-staging]: https://ohif-staging.netlify.com
[netlify-stage]: https://ohif-stage.netlify.com
[netlify-prod]: https://ohif-prod.netlify.com
[ohif-dev]: https://viewer-dev.ohif.org
[ohif-stage]: https://viewer-stage.ohif.org
[ohif-prod]: https://viewer-prod.ohif.org
<!-- prettier-ignore-end -->
+2 -2
View File
@@ -7,8 +7,8 @@ like to discuss the changes you intend to make to clarify where or how they
should be implemented, please don't hesitate to create a new issue. At a
minimum, you may want to read the following documentation:
- [Essentials: Getting Started](./essentials/getting-started.md)
- [Advanced: Architecture](./advanced/architecture.md)
- [Essentials: Getting Started](/essentials/getting-started.md)
- [Advanced: Architecture](/advanced/architecture.md)
Pull requests that are:
+109
View File
@@ -0,0 +1,109 @@
# Our Process
Our process is a living, breathing thing. We strive to have regular [retrospectives][retrospective] that help us
shape and adapt our process to our team's current needs. This document attempts to capture the broad strokes of
that process in an effort to:
- Strengthen community member involvement and understanding
- Welcome feedback and helpful suggestions
## Overview
- [Issue Triage](#issue-triage)
- [Issue Curation ("backlog grooming")](#issue-curation-backlog-grooming)
- [Contributions (Pull Requests)](#contributions-pull-requests)
- [Releases](#releases)
_Include issue lifecycle diagram_
## Issue Triage
[GitHub issues][gh-issues] are the best way to provide feedback, ask questions, and suggest changes to the OHIF Viewer's core
team. Community issues generally fall into one of three categories, and are marked with a `triage` label when created.
|Issue Template Name | Description |
|------------------------|-----------------------------------------------------------------------------------------|
|Community: Report 🐛 | Describe a new issue; Provide steps to reproduce; Expected versus actual result? |
|Community: Request ✋ | Describe a proposed new feature. Why should it be implemented? What is the impact/value? |
|Community: Question ❓ | Seek clarification or assistance relevant to the repository. |
_table 1. issue template names and descriptions_
Issues that require `triage` are akin to support tickets. As this is often our first contact with would-be adopters and
contributors, it's important that we strive for timely responses and satisfactory resolutions. We attempt to accomplish this
by:
1. Responding to issues requiring `triage` at least once a week
2. Create new "official issues" from "community issues"
3. Provide clear guidance and next steps (when applicable)
4. Regularly clean up old (stale) issues
> :pencil: Less obviously, patterns in the issues being reported can highlight areas that need improvement. For example, users often have
difficulty navigating CORS issues when deploying the OHIF Viewer -- how do we best reduce our ticket volume for this issue?
### Backlogged Issues
Community issues serve as vehicles of discussion that lead us to "backlogged issues". Backlogged issues are the distilled and actionable information extracted from community issues. They contain the scope and requirements necessary for hand-off to a core-team (or community) contributor ^_^
|Category| Description| Labels|
|--------|-----| ---------|
| Bugs | An issue with steps that produce a bug (an unexpected result). | [Bug: Verified 🐛][label-bug] |
| Stories| A feature/enhancement with a clear benefit, boundaries, and requirements. | [Story 🙌][label-story] |
| Tasks | Changes that improve [UX], [DX], or test coverage; but don't impact application behavior | [Task: CI/Tooling 🤖][label-tooling], [Task: Docs 📖][label-docs], [Task: Refactor 🛠][label-refactor], [Task: Tests 🔬][label-tests] |
_table 2. backlogged issue types ([full list of labels][gh-labels])_
## Issue Curation (["backlog grooming"][groom-backlog])
If a [GitHub issue][gh-issues] has a `bug`, `story`, or `task` label; it's on our backlog. If an issue is on our backlog, it means we are, at the very least, committed to reviewing any community drafted Pull Requests to complete the issue. If you're interested in seeing an issue completed but don't know where to start, please don't hesitate to leave a comment!
While we don't yet have a long-term or quarterly road map, we do regularly add items to our ["Active Development" GitHub Project Board][gh-board]. Items on this project board are either in active development by Core Team members, or queued up for development as in-progress items are completed.
> :pencil: Want to contribute but not sure where to start? Check out [Up for grabs][label-grabs] issues and our [Contributing documentation][contributing-docs]
## Contributions (Pull Requests)
Incoming Pull Requests (PRs) are triaged using the following labels. Code review is performed on all PRs where the bug fix or added functionality is deemed appropriate:
- PR: Bug Fix - Describes a PR filed to address a Bug.
- PR: Draft - Describes a PR filed to gather early feedback from the core team, but which is not intended for merging in the short term.
- Awaiting Response - Describes a PR where the core team is waiting for additional information from the author. This may occur before or after code review.
- Awaiting Review - Describes a PR where the core team has not yet performed a code review.
- Awaiting Revisions - Following code review, this label is applied until the author has made sufficient changes.
We rely on GitHub Checks and integrations with third party services to evaluate changes in code quality and test coverage. Tests must pass before a PR can be merged to master, and code quality and test coverage must not changed by a significant margin. For some repositories, visual screenshot-based tests are also included, and video recordings of end-to-end tests are stored for later review.
[You can read more about our continous integration efforts here](/continous-integration.md)
## Releases
Releases are made automatically based on the type of commits which have been merged (major.minor.patch). Releases are automatically pushed to NPM. Release notes are automatically generated. Users can subscribe to GitHub and NPM releases.
Important announcements are made on GitHub, tagged as Announcement, and pinned so that they remain at the top of the Issue page.
The Core team occasionally performs full manual testing to begin the process of releasing a Stable version. Once testing is complete, the known issues are addressed and a Stable version
<!--
LINKS
-->
<!-- prettier-ignore-start -->
[groom-backlog]: https://www.agilealliance.org/glossary/backlog-grooming
[retrospective]: https://www.atlassian.com/team-playbook/plays/retrospective
[gh-issues]: https://github.com/OHIF/Viewers/issues/new/choose
[gh-labels]: https://github.com/OHIF/Viewers/labels
[label-story]: https://github.com/OHIF/Viewers/labels/Story%20%3Araised_hands%3A
[label-tooling]: https://github.com/OHIF/Viewers/labels/Task%3A%20CI%2FTooling%20%3Arobot%3A
[label-docs]: https://github.com/OHIF/Viewers/labels/Task%3A%20Docs%20%3Abook%3A
[label-refactor]: https://github.com/OHIF/Viewers/labels/Task%3A%20Refactor%20%3Ahammer_and_wrench%3A
[label-tests]: https://github.com/OHIF/Viewers/labels/Task%3A%20Tests%20%3Amicroscope%3A
[label-bug]: https://github.com/OHIF/Viewers/labels/Bug%3A%20Verified%20%3Abug%3A
[gh-board]: https://github.com/OHIF/Viewers/projects/4
[label-grabs]: https://github.com/OHIF/Viewers/issues?q=is%3Aissue+is%3Aopen+label%3A%22Up+For+Grabs+%3Araising_hand_woman%3A%22
[contributing-docs]: https://docs.ohif.org/contributing/
<!-- prettier-ignore-end -->
+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.6.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@1.5.1...@ohif/extension-cornerstone@1.6.0) (2019-11-19)
### Features
* New dialog service ([#1202](https://github.com/OHIF/Viewers/issues/1202)) ([f65639c](https://github.com/OHIF/Viewers/commit/f65639c2b0dab01decd20cab2cef4263cb4fab37))
## [1.5.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@1.5.0...@ohif/extension-cornerstone@1.5.1) (2019-11-15)
**Note:** Version bump only for package @ohif/extension-cornerstone
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/extension-cornerstone",
"version": "1.5.1",
"version": "1.6.0",
"description": "OHIF extension for Cornerstone",
"author": "OHIF",
"license": "MIT",
+14 -2
View File
@@ -23,6 +23,17 @@ const TOOLBAR_BUTTON_TYPES = {
BUILT_IN: 'builtIn',
};
const TOOLBAR_BUTTON_BEHAVIORS = {
CINE: 'CINE',
DOWNLOAD_SCREEN_SHOT: 'DOWNLOAD_SCREEN_SHOT',
};
/* TODO: Export enums through a extension manager. */
const enums = {
TOOLBAR_BUTTON_TYPES,
TOOLBAR_BUTTON_BEHAVIORS,
};
const definitions = [
{
id: 'StackScroll',
@@ -102,7 +113,7 @@ const definitions = [
//
type: TOOLBAR_BUTTON_TYPES.BUILT_IN,
options: {
behavior: 'CINE',
behavior: TOOLBAR_BUTTON_BEHAVIORS.CINE,
},
},
{
@@ -220,7 +231,8 @@ const definitions = [
//
type: TOOLBAR_BUTTON_TYPES.BUILT_IN,
options: {
behavior: 'DOWNLOAD_SCREEN_SHOT',
behavior: TOOLBAR_BUTTON_BEHAVIORS.DOWNLOAD_SCREEN_SHOT,
togglable: true,
},
},
],
+32
View File
@@ -3,6 +3,38 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [0.53.11](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.53.10...@ohif/extension-vtk@0.53.11) (2019-11-19)
**Note:** Version bump only for package @ohif/extension-vtk
## [0.53.10](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.53.9...@ohif/extension-vtk@0.53.10) (2019-11-19)
**Note:** Version bump only for package @ohif/extension-vtk
## [0.53.9](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.53.8...@ohif/extension-vtk@0.53.9) (2019-11-18)
**Note:** Version bump only for package @ohif/extension-vtk
## [0.53.8](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.53.7...@ohif/extension-vtk@0.53.8) (2019-11-15)
**Note:** Version bump only for package @ohif/extension-vtk
## [0.53.7](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.53.6...@ohif/extension-vtk@0.53.7) (2019-11-15)
**Note:** Version bump only for package @ohif/extension-vtk
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/extension-vtk",
"version": "0.53.7",
"version": "0.53.11",
"description": "OHIF extension for VTK.js",
"author": "OHIF",
"license": "MIT",
@@ -52,8 +52,8 @@
"react-vtkjs-viewport": "^0.3.9"
},
"devDependencies": {
"@ohif/core": "^1.9.1",
"@ohif/ui": "^0.62.2",
"@ohif/core": "^1.12.0",
"@ohif/ui": "^0.64.0",
"cornerstone-tools": "^4.0.9",
"cornerstone-wado-image-loader": "^3.0.0",
"dcmjs": "^0.6.1",
+33
View File
@@ -3,6 +3,39 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.12.0](https://github.com/OHIF/Viewers/compare/@ohif/core@1.11.0...@ohif/core@1.12.0) (2019-11-19)
### Features
* New dialog service ([#1202](https://github.com/OHIF/Viewers/issues/1202)) ([f65639c](https://github.com/OHIF/Viewers/commit/f65639c2b0dab01decd20cab2cef4263cb4fab37))
# [1.11.0](https://github.com/OHIF/Viewers/compare/@ohif/core@1.10.0...@ohif/core@1.11.0) (2019-11-19)
### Features
* Issue 879 viewer route query param not filtering but promoting ([#1141](https://github.com/OHIF/Viewers/issues/1141)) ([b17f753](https://github.com/OHIF/Viewers/commit/b17f753e6222045252ef885e40233681541a32e1)), closes [#1118](https://github.com/OHIF/Viewers/issues/1118)
# [1.10.0](https://github.com/OHIF/Viewers/compare/@ohif/core@1.9.1...@ohif/core@1.10.0) (2019-11-15)
### Features
* Inject into Extension Modules / improve tests ([f63d8a7](https://github.com/OHIF/Viewers/commit/f63d8a73d867ad9dfd8ee0cad74edce180eb34f0))
## [1.9.1](https://github.com/OHIF/Viewers/compare/@ohif/core@1.9.0...@ohif/core@1.9.1) (2019-11-15)
**Note:** Version bump only for package @ohif/core
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/core",
"version": "1.9.1",
"version": "1.12.0",
"description": "Generic business logic for web-based medical imaging applications",
"author": "OHIF Core Team",
"license": "MIT",
@@ -110,7 +110,9 @@ export default class ExtensionManager {
}
try {
const extensionModule = getModuleFn();
const extensionModule = getModuleFn({
servicesManager: this._servicesManager,
});
if (!extensionModule) {
log.warn(
@@ -40,6 +40,30 @@ describe('ExtensionManager.js', () => {
});
describe('registerExtension()', () => {
it('calls preRegistration() for extension', () => {
// SUT
const fakeExtension = { one: '1', preRegistration: jest.fn() };
extensionManager.registerExtension(fakeExtension);
// Assert
expect(fakeExtension.preRegistration.mock.calls.length).toBe(1);
});
it('calls preRegistration() passing configuration and servicesManager instance for extension', () => {
const configuration = { config: 'Some configuration' };
extensionManager._servicesManager = { services: { TestService: {} } };
// SUT
const fakeExtension = { one: '1', preRegistration: jest.fn() };
extensionManager.registerExtension(fakeExtension, configuration);
// Assert
expect(fakeExtension.preRegistration.mock.calls[0][0]).toEqual({
servicesManager: extensionManager._servicesManager,
configuration,
});
});
it('logs a warning if the extension is null or undefined', () => {
const undefinedExtension = undefined;
const nullExtension = null;
@@ -110,6 +134,25 @@ describe('ExtensionManager.js', () => {
);
});
it('successfully passes a servicesManager instance to each module', () => {
extensionManager._servicesManager = { services: { TestService: {} } };
const extension = {
id: 'hello-world',
getViewportModule: jest.fn(),
getSopClassHandlerModule: jest.fn(),
getPanelModule: jest.fn(),
getToolbarModule: jest.fn(),
getCommandsModule: jest.fn(),
};
extensionManager.registerExtension(extension);
expect(extension.getViewportModule.mock.calls[0][0]).toEqual({
servicesManager: extensionManager._servicesManager,
});
});
it('successfully registers a module for each module type', () => {
const extension = {
id: 'hello-world',
+7 -1
View File
@@ -19,7 +19,11 @@ import ui from './ui';
import user from './user.js';
import utils from './utils/';
import { createUINotificationService, createUIModalService } from './services';
import {
createUINotificationService,
createUIModalService,
createUIDialogService,
} from './services';
const OHIF = {
MODULE_TYPES,
@@ -48,6 +52,7 @@ const OHIF = {
//
createUINotificationService,
createUIModalService,
createUIDialogService,
};
export {
@@ -76,6 +81,7 @@ export {
//
createUINotificationService,
createUIModalService,
createUIDialogService,
};
export { OHIF };
+1
View File
@@ -12,6 +12,7 @@ describe('Top level exports', () => {
//
'createUINotificationService',
'createUIModalService',
'createUIDialogService',
//
'utils',
'studies',
+41 -1
View File
@@ -1,9 +1,49 @@
import log from './../log.js';
export default class ServicesManager {
constructor() {
this.services = {};
this.registeredServiceNames = [];
}
register(service) {
/**
*
* @param {Object} service
*/
registerService(service) {
if (!service) {
log.warn(
'Attempting to register a null/undefined service. Exiting early.'
);
return;
}
let serviceName = service.name;
if (!serviceName) {
log.warn(`Service name not set. Exiting early.`);
return;
}
if (this.registeredServiceNames.includes(serviceName)) {
log.warn(
`Extension name ${serviceName} has already been registered. Exiting before duplicating services.`
);
return;
}
this.services[service.name] = service;
// Track service registration
this.registeredServiceNames.push(serviceName);
}
/**
* An array of services.
*
* @param {Object[]} services - Array of services
*/
registerServices(services) {
services.forEach(service => this.registerService(service));
}
}
@@ -0,0 +1,70 @@
import ServicesManager from './ServicesManager.js';
import log from '../log.js';
jest.mock('./../log.js');
describe('ServicesManager.js', () => {
let servicesManager;
beforeEach(() => {
servicesManager = new ServicesManager();
log.warn.mockClear();
jest.clearAllMocks();
});
describe('registerServices()', () => {
it('calls registerService() for each service', () => {
servicesManager.registerService = jest.fn();
const fakeServices = [
{ name: 'UINotificationTestService', hide: jest.fn() },
{ name: 'UIModalTestService', hide: jest.fn() },
];
servicesManager.registerServices(fakeServices);
expect(servicesManager.registerService.mock.calls.length).toBe(2);
});
});
describe('registerService()', () => {
it('logs a warning if the service is null or undefined', () => {
const undefinedService = undefined;
const nullService = null;
servicesManager.registerService(undefinedService);
servicesManager.registerService(nullService);
expect(log.warn.mock.calls.length).toBe(2);
});
it('logs a warning if the service does not have a name', () => {
const serviceWithEmptyName = { name: '', hide: jest.fn() };
const serviceWithoutName = { hide: jest.fn() };
servicesManager.registerService(serviceWithEmptyName);
servicesManager.registerService(serviceWithoutName);
expect(log.warn.mock.calls.length).toBe(2);
});
it('tracks which services have been registered', () => {
const service = {
name: 'UINotificationService',
};
servicesManager.registerService(service);
expect(servicesManager.registeredServiceNames).toContain(service.name);
});
it('logs a warning if the service has an name that has already been registered', () => {
const service = { name: 'UINotificationService' };
servicesManager.registerService(service);
servicesManager.registerService(service);
expect(log.warn.mock.calls.length).toBe(1);
});
});
});
@@ -0,0 +1,125 @@
/**
* A UI Element
*
* @typedef {ReactElement|HTMLElement} DialogContent
*/
/**
* A UI Position
*
* @typedef {Object} ElementPosition
* @property {number} top -
* @property {number} left -
* @property {number} right -
* @property {number} bottom -
*/
/**
* UI Dialog
*
* @typedef {Object} DialogProps
* @property {string} id -
* @property {DialogContent} content -
* @property {boolean} isDraggable -
* @property {ElementPosition} defaultPosition -
* @property {ElementPosition} position -
* @property {Function} onSubmit -
* @property {Function} onClose -
* @property {Function} onStart -
* @property {Function} onStop -
* @property {Function} onDrag -
*/
const uiDialogServicePublicAPI = {
name: 'UIDialogService',
dismiss,
dismissAll,
create,
setServiceImplementation,
};
const uiDialogServiceImplementation = {
_dismiss: () => console.warn('dismiss() NOT IMPLEMENTED'),
_dismissAll: () => console.warn('dismissAll() NOT IMPLEMENTED'),
_create: () => console.warn('create() NOT IMPLEMENTED'),
};
function createUIDialogService() {
return uiDialogServicePublicAPI;
}
/**
* Show a new UI dialog;
*
* @param {DialogProps} props { id, content, onSubmit, onClose, onStart, onDrag, onStop, isDraggable, defaultPosition, position }
*/
function create({
id,
content,
onSubmit,
onClose,
onStart,
onDrag,
onStop,
isDraggable,
defaultPosition,
position,
}) {
return uiDialogServiceImplementation._create({
id,
content,
onSubmit,
onClose,
onStart,
onDrag,
onStop,
isDraggable,
defaultPosition,
position,
});
}
/**
* Destroys all dialogs, if any
*
* @returns void
*/
function dismissAll() {
return uiDialogServiceImplementation._dismissAll();
}
/**
* Destroy the dialog, if currently created
*
* @returns void
*/
function dismiss({ id }) {
return uiDialogServiceImplementation._dismiss({ id });
}
/**
*
*
* @param {*} {
* dismiss: dismissImplementation,
* dismissAll: dismissAllImplementation,
* create: createImplementation,
* }
*/
function setServiceImplementation({
dismiss: dismissImplementation,
dismissAll: dismissAllImplementation,
create: createImplementation,
}) {
if (dismissImplementation) {
uiDialogServiceImplementation._dismiss = dismissImplementation;
}
if (dismissAllImplementation) {
uiDialogServiceImplementation._dismissAll = dismissAllImplementation;
}
if (createImplementation) {
uiDialogServiceImplementation._create = createImplementation;
}
}
export default createUIDialogService;
@@ -8,14 +8,11 @@
* UI Modal
*
* @typedef {Object} ModalProps
* @property {string} [header=null] -
* @property {string} [footer=null] -
* @property {string} [backdrop=false] -
* @property {string} [keyboard=false] -
* @property {number} [show=true] -
* @property {string} [closeButton=true] -
* @property {boolean} [shouldCloseOnEsc=false] -
* @property {boolean} [isOpen=true] -
* @property {boolean} [closeButton=true] -
* @property {string} [title=null] - 'Modal Title'
* @property {boolean} [customClassName=null] - '.ModalClass'
* @property {string} [customClassName=null] - '.ModalClass'
*/
const uiModalServicePublicAPI = {
@@ -38,16 +35,13 @@ function createUIModalService() {
* Show a new UI modal;
*
* @param {Modal} component React component
* @param {ModalProps} props { header, footer, backdrop, keyboard, show, closeButton, title, customClassName }
* @param {ModalProps} props { shouldCloseOnEsc, isOpen, closeButton, title, customClassName }
*/
function show(
component,
props = {
header: null,
footer: null,
backdrop: false,
keyboard: false,
show: true,
shouldCloseOnEsc: false,
isOpen: true,
closeButton: true,
title: null,
customClassName: null,
+7 -1
View File
@@ -1,5 +1,11 @@
import ServicesManager from './ServicesManager.js';
import createUINotificationService from './UINotificationService';
import createUIModalService from './UIModalService';
import createUIDialogService from './UIDialogService';
export { createUINotificationService, createUIModalService, ServicesManager };
export {
createUINotificationService,
createUIModalService,
createUIDialogService,
ServicesManager,
};
@@ -40,7 +40,7 @@ export default class RetrieveMetadataLoader {
if (result && result.length) {
break; // closes iterator in case data is retrieved successfully
}
} catch (e) {}
} catch (e) { }
}
if (loaders.next().done && !result) {
@@ -51,8 +51,8 @@ export default class RetrieveMetadataLoader {
}
// Methods to be overwrite
async configLoad() {}
async preLoad() {}
async load(preLoadData) {}
async posLoad(loadData) {}
async configLoad() { }
async preLoad() { }
async load(preLoadData) { }
async posLoad(loadData) { }
}
+413
View File
@@ -0,0 +1,413 @@
import DICOMWeb from '../DICOMWeb/';
const WADOProxy = {
convertURL: (url, server) => {
// TODO: Remove all WADOProxy stuff from this file
return url;
},
};
function parseFloatArray(obj) {
const result = [];
if (!obj) {
return result;
}
const objs = obj.split('\\');
for (let i = 0; i < objs.length; i++) {
result.push(parseFloat(objs[i]));
}
return result;
}
/**
* Create a plain JS object that describes a study (a study descriptor object)
* @param {Object} server Object with server configuration parameters
* @param {Object} aSopInstance a SOP Instance from which study information will be added
*/
function createStudy(server, aSopInstance) {
// TODO: Pass a reference ID to the server instead of including the URLs here
return {
seriesList: [],
seriesMap: Object.create(null),
seriesLoader: null,
wadoUriRoot: server.wadoUriRoot,
wadoRoot: server.wadoRoot,
qidoRoot: server.qidoRoot,
patientName: DICOMWeb.getName(aSopInstance['00100010']),
patientId: DICOMWeb.getString(aSopInstance['00100020']),
patientAge: DICOMWeb.getNumber(aSopInstance['00101010']),
patientSize: DICOMWeb.getNumber(aSopInstance['00101020']),
patientWeight: DICOMWeb.getNumber(aSopInstance['00101030']),
accessionNumber: DICOMWeb.getString(aSopInstance['00080050']),
studyDate: DICOMWeb.getString(aSopInstance['00080020']),
modalities: DICOMWeb.getString(aSopInstance['00080061']),
studyDescription: DICOMWeb.getString(aSopInstance['00081030']),
imageCount: DICOMWeb.getString(aSopInstance['00201208']),
studyInstanceUid: DICOMWeb.getString(aSopInstance['0020000D']),
institutionName: DICOMWeb.getString(aSopInstance['00080080']),
};
}
/** Returns a WADO url for an instance
*
* @param studyInstanceUid
* @param seriesInstanceUid
* @param sopInstanceUid
* @returns {string}
*/
function buildInstanceWadoUrl(
server,
studyInstanceUid,
seriesInstanceUid,
sopInstanceUid
) {
// TODO: This can be removed, since DICOMWebClient has the same function. Not urgent, though
const params = [];
params.push('requestType=WADO');
params.push(`studyUID=${studyInstanceUid}`);
params.push(`seriesUID=${seriesInstanceUid}`);
params.push(`objectUID=${sopInstanceUid}`);
params.push('contentType=application/dicom');
params.push('transferSyntax=*');
const paramString = params.join('&');
return `${server.wadoUriRoot}?${paramString}`;
}
function buildInstanceWadoRsUri(
server,
studyInstanceUid,
seriesInstanceUid,
sopInstanceUid
) {
return `${server.wadoRoot}/studies/${studyInstanceUid}/series/${seriesInstanceUid}/instances/${sopInstanceUid}`;
}
function buildInstanceFrameWadoRsUri(
server,
studyInstanceUid,
seriesInstanceUid,
sopInstanceUid,
frame
) {
const baseWadoRsUri = buildInstanceWadoRsUri(
server,
studyInstanceUid,
seriesInstanceUid,
sopInstanceUid
);
frame = frame != null || 1;
return `${baseWadoRsUri}/frames/${frame}`;
}
function getFrameIncrementPointer(element) {
const frameIncrementPointerNames = {
'00181065': 'frameTimeVector',
'00181063': 'frameTime',
};
if (!element || !element.Value || !element.Value.length) {
return;
}
const value = element.Value[0];
return frameIncrementPointerNames[value];
}
function getRadiopharmaceuticalInfo(instance) {
const modality = DICOMWeb.getString(instance['00080060']);
if (modality !== 'PT') {
return;
}
const radiopharmaceuticalInfo = instance['00540016'];
if (
radiopharmaceuticalInfo === undefined ||
!radiopharmaceuticalInfo.Value ||
!radiopharmaceuticalInfo.Value.length
) {
return;
}
const firstPetRadiopharmaceuticalInfo = radiopharmaceuticalInfo.Value[0];
return {
radiopharmaceuticalStartTime: DICOMWeb.getString(
firstPetRadiopharmaceuticalInfo['00181072']
),
radionuclideTotalDose: DICOMWeb.getNumber(
firstPetRadiopharmaceuticalInfo['00181074']
),
radionuclideHalfLife: DICOMWeb.getNumber(
firstPetRadiopharmaceuticalInfo['00181075']
),
};
}
/**
* Parses the SourceImageSequence, if it exists, in order
* to return a ReferenceSOPInstanceUID. The ReferenceSOPInstanceUID
* is used to refer to this image in any accompanying DICOM-SR documents.
*
* @param instance
* @returns {String} The ReferenceSOPInstanceUID
*/
function getSourceImageInstanceUid(instance) {
// TODO= Parse the whole Source Image Sequence
// This is a really poor workaround for now.
// Later we should probably parse the whole sequence.
var SourceImageSequence = instance['00082112'];
if (
SourceImageSequence &&
SourceImageSequence.Value &&
SourceImageSequence.Value.length &&
SourceImageSequence.Value[0]['00081155'].Value
) {
return SourceImageSequence.Value[0]['00081155'].Value[0];
}
}
async function makeSOPInstance(server, study, instance) {
const { studyInstanceUid } = study;
const seriesInstanceUid = DICOMWeb.getString(instance['0020000E']);
let series = study.seriesMap[seriesInstanceUid];
if (!series) {
series = {
seriesInstanceUid,
seriesDescription: DICOMWeb.getString(instance['0008103E']),
modality: DICOMWeb.getString(instance['00080060']),
seriesNumber: DICOMWeb.getNumber(instance['00200011']),
seriesDate: DICOMWeb.getString(instance['00080021']),
seriesTime: DICOMWeb.getString(instance['00080031']),
instances: [],
};
study.seriesMap[seriesInstanceUid] = series;
study.seriesList.push(series);
}
const sopInstanceUid = DICOMWeb.getString(instance['00080018']);
const wadouri = buildInstanceWadoUrl(
server,
studyInstanceUid,
seriesInstanceUid,
sopInstanceUid
);
const baseWadoRsUri = buildInstanceWadoRsUri(
server,
studyInstanceUid,
seriesInstanceUid,
sopInstanceUid
);
const wadorsuri = buildInstanceFrameWadoRsUri(
server,
studyInstanceUid,
seriesInstanceUid,
sopInstanceUid
);
const sopInstance = {
imageType: DICOMWeb.getString(instance['00080008']),
sopClassUid: DICOMWeb.getString(instance['00080016']),
modality: DICOMWeb.getString(instance['00080060']),
sopInstanceUid,
instanceNumber: DICOMWeb.getNumber(instance['00200013']),
imagePositionPatient: DICOMWeb.getString(instance['00200032']),
imageOrientationPatient: DICOMWeb.getString(instance['00200037']),
frameOfReferenceUID: DICOMWeb.getString(instance['00200052']),
sliceLocation: DICOMWeb.getNumber(instance['00201041']),
samplesPerPixel: DICOMWeb.getNumber(instance['00280002']),
photometricInterpretation: DICOMWeb.getString(instance['00280004']),
planarConfiguration: DICOMWeb.getNumber(instance['00280006']),
rows: DICOMWeb.getNumber(instance['00280010']),
columns: DICOMWeb.getNumber(instance['00280011']),
pixelSpacing: DICOMWeb.getString(instance['00280030']),
pixelAspectRatio: DICOMWeb.getString(instance['00280034']),
bitsAllocated: DICOMWeb.getNumber(instance['00280100']),
bitsStored: DICOMWeb.getNumber(instance['00280101']),
highBit: DICOMWeb.getNumber(instance['00280102']),
pixelRepresentation: DICOMWeb.getNumber(instance['00280103']),
smallestPixelValue: DICOMWeb.getNumber(instance['00280106']),
largestPixelValue: DICOMWeb.getNumber(instance['00280107']),
windowCenter: DICOMWeb.getString(instance['00281050']),
windowWidth: DICOMWeb.getString(instance['00281051']),
rescaleIntercept: DICOMWeb.getNumber(instance['00281052']),
rescaleSlope: DICOMWeb.getNumber(instance['00281053']),
rescaleType: DICOMWeb.getNumber(instance['00281054']),
sourceImageInstanceUid: getSourceImageInstanceUid(instance),
laterality: DICOMWeb.getString(instance['00200062']),
viewPosition: DICOMWeb.getString(instance['00185101']),
acquisitionDateTime: DICOMWeb.getString(instance['0008002A']),
numberOfFrames: DICOMWeb.getNumber(instance['00280008']),
frameIncrementPointer: getFrameIncrementPointer(instance['00280009']),
frameTime: DICOMWeb.getNumber(instance['00181063']),
frameTimeVector: parseFloatArray(DICOMWeb.getString(instance['00181065'])),
sliceThickness: DICOMWeb.getNumber(instance['00180050']),
spacingBetweenSlices: DICOMWeb.getString(instance['00180088']),
lossyImageCompression: DICOMWeb.getString(instance['00282110']),
derivationDescription: DICOMWeb.getString(instance['00282111']),
lossyImageCompressionRatio: DICOMWeb.getString(instance['00282112']),
lossyImageCompressionMethod: DICOMWeb.getString(instance['00282114']),
echoNumber: DICOMWeb.getString(instance['00180086']),
contrastBolusAgent: DICOMWeb.getString(instance['00180010']),
radiopharmaceuticalInfo: getRadiopharmaceuticalInfo(instance),
baseWadoRsUri: baseWadoRsUri,
wadouri: WADOProxy.convertURL(wadouri, server),
wadorsuri: WADOProxy.convertURL(wadorsuri, server),
wadoRoot: server.wadoRoot,
imageRendering: server.imageRendering,
thumbnailRendering: server.thumbnailRendering,
};
// Get additional information if the instance uses "PALETTE COLOR" photometric interpretation
if (sopInstance.photometricInterpretation === 'PALETTE COLOR') {
const redPaletteColorLookupTableDescriptor = parseFloatArray(
DICOMWeb.getString(instance['00281101'])
);
const greenPaletteColorLookupTableDescriptor = parseFloatArray(
DICOMWeb.getString(instance['00281102'])
);
const bluePaletteColorLookupTableDescriptor = parseFloatArray(
DICOMWeb.getString(instance['00281103'])
);
const palettes = await getPaletteColors(
server,
instance,
redPaletteColorLookupTableDescriptor
);
if (palettes) {
if (palettes.uid) {
sopInstance.paletteColorLookupTableUID = palettes.uid;
}
sopInstance.redPaletteColorLookupTableData = palettes.red;
sopInstance.greenPaletteColorLookupTableData = palettes.green;
sopInstance.bluePaletteColorLookupTableData = palettes.blue;
sopInstance.redPaletteColorLookupTableDescriptor = redPaletteColorLookupTableDescriptor;
sopInstance.greenPaletteColorLookupTableDescriptor = greenPaletteColorLookupTableDescriptor;
sopInstance.bluePaletteColorLookupTableDescriptor = bluePaletteColorLookupTableDescriptor;
}
}
series.instances.push(sopInstance);
return sopInstance;
}
function getPaletteColor(server, instance, tag, lutDescriptor) {
const numLutEntries = lutDescriptor[0];
const bits = lutDescriptor[2];
let uri = WADOProxy.convertURL(instance[tag].BulkDataURI, server);
// TODO: Workaround for dcm4chee behind SSL-terminating proxy returning
// incorrect bulk data URIs
if (server.wadoRoot.indexOf('https') === 0 && !uri.includes('https')) {
uri = uri.replace('http', 'https');
}
const config = {
url: server.wadoRoot, //BulkDataURI is absolute, so this isn't used
headers: DICOMWeb.getAuthorizationHeader(server),
};
const dicomWeb = new api.DICOMwebClient(config);
const options = {
BulkDataURI: uri,
};
const readUInt16 = (byteArray, position) => {
return byteArray[position] + byteArray[position + 1] * 256;
};
const arrayBufferToPaletteColorLUT = result => {
const arraybuffer = result[0];
const byteArray = new Uint8Array(arraybuffer);
const lut = [];
for (let i = 0; i < numLutEntries; i++) {
if (bits === 16) {
lut[i] = readUInt16(byteArray, i * 2);
} else {
lut[i] = byteArray[i];
}
}
return lut;
};
return dicomWeb.retrieveBulkData(options).then(arrayBufferToPaletteColorLUT);
}
/**
* Fetch palette colors for instances with "PALETTE COLOR" photometricInterpretation.
*
* @param server {Object} Current server;
* @param instance {Object} The retrieved instance metadata;
* @returns {String} The ReferenceSOPInstanceUID
*/
async function getPaletteColors(server, instance, lutDescriptor) {
let paletteUID = DICOMWeb.getString(instance['00281199']);
return new Promise((resolve, reject) => {
let entry;
if (paletteColorCache.isValidUID(paletteUID)) {
entry = paletteColorCache.get(paletteUID);
if (entry) {
return resolve(entry);
}
}
// no entry in cache... Fetch remote data.
const r = getPaletteColor(server, instance, '00281201', lutDescriptor);
const g = getPaletteColor(server, instance, '00281202', lutDescriptor);
const b = getPaletteColor(server, instance, '00281203', lutDescriptor);
const promises = [r, g, b];
Promise.all(promises).then(args => {
entry = {
red: args[0],
green: args[1],
blue: args[2],
};
// when paletteUID is present, the entry can be cached...
entry.uid = paletteUID;
paletteColorCache.add(entry);
resolve(entry);
});
});
}
/**
* Add a list of SOP Instances to a given study object descriptor
* @param {Object} server Object with server configuration parameters
* @param {Object} study The study descriptor to which the given SOP instances will be added
* @param {Array} sopInstanceList A list of SOP instance objects
*/
async function addInstancesToStudy(server, study, sopInstanceList) {
return Promise.all(
sopInstanceList.map(function(sopInstance) {
return makeSOPInstance(server, study, sopInstance);
})
);
}
const createStudyFromSOPInstanceList = async (server, sopInstanceList) => {
if (Array.isArray(sopInstanceList) && sopInstanceList.length > 0) {
const firstSopInstance = sopInstanceList[0];
const study = createStudy(server, firstSopInstance);
await addInstancesToStudy(server, study, sopInstanceList);
return study;
}
throw new Error('Failed to create study out of provided SOP instance list');
};
export { createStudyFromSOPInstanceList, addInstancesToStudy };
+11
View File
@@ -3,6 +3,17 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [0.52.1](https://github.com/OHIF/Viewers/compare/@ohif/i18n@0.52.0...@ohif/i18n@0.52.1) (2019-11-18)
### Bug Fixes
* minor date picker UX improvements ([813ee5e](https://github.com/OHIF/Viewers/commit/813ee5ed4d78b7bda234922d5f3389efe346451c))
# [0.52.0](https://github.com/OHIF/Viewers/compare/@ohif/i18n@0.51.0...@ohif/i18n@0.52.0) (2019-11-06)
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/i18n",
"version": "0.52.0",
"version": "0.52.1",
"description": "Internationalization library for The OHIF Viewer",
"author": "OHIF",
"license": "MIT",
@@ -1,4 +1,5 @@
{
"Close": "Close",
"Image": "Image",
"Layout": "Layout",
"Measurements": "Measurements",
@@ -0,0 +1,5 @@
{
"Clear dates": "Clear dates",
"End Date": "End Date",
"Start Date": "Start Date"
}
+2
View File
@@ -2,6 +2,7 @@ import AboutModal from './AboutModal.json';
import Buttons from './Buttons.json';
import CineDialog from './CineDialog.json';
import Common from './Common.json';
import DatePicker from './DatePicker.json';
import Header from './Header.json';
import MeasurementTable from './MeasurementTable.json';
import StudyList from './StudyList.json';
@@ -13,6 +14,7 @@ export default {
Buttons,
CineDialog,
Common,
DatePicker,
Header,
MeasurementTable,
StudyList,
@@ -1,4 +1,5 @@
{
"Close": "Fechar",
"Image": "Imagem",
"Layout": "Layout",
"More": "Mais",
@@ -0,0 +1,5 @@
{
"Clear dates": "Limpar datas",
"End Date": "Data Final",
"Start Date": "Data Inicial"
}
+2
View File
@@ -2,6 +2,7 @@ import AboutModal from './AboutModal.json';
import Buttons from './Buttons.json';
import CineDialog from './CineDialog.json';
import Common from './Common.json';
import DatePicker from './DatePicker.json';
import Header from './Header.json';
import UserPreferencesModal from './UserPreferencesModal.json';
@@ -11,6 +12,7 @@ export default {
Buttons,
CineDialog,
Common,
DatePicker,
Header,
UserPreferencesModal,
},
+41
View File
@@ -3,6 +3,47 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [0.64.0](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.63.0...@ohif/ui@0.64.0) (2019-11-19)
### Features
* New dialog service ([#1202](https://github.com/OHIF/Viewers/issues/1202)) ([f65639c](https://github.com/OHIF/Viewers/commit/f65639c2b0dab01decd20cab2cef4263cb4fab37))
# [0.63.0](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.62.4...@ohif/ui@0.63.0) (2019-11-19)
### Features
* Issue 879 viewer route query param not filtering but promoting ([#1141](https://github.com/OHIF/Viewers/issues/1141)) ([b17f753](https://github.com/OHIF/Viewers/commit/b17f753e6222045252ef885e40233681541a32e1)), closes [#1118](https://github.com/OHIF/Viewers/issues/1118)
## [0.62.4](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.62.3...@ohif/ui@0.62.4) (2019-11-18)
### Bug Fixes
* minor date picker UX improvements ([813ee5e](https://github.com/OHIF/Viewers/commit/813ee5ed4d78b7bda234922d5f3389efe346451c))
## [0.62.3](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.62.2...@ohif/ui@0.62.3) (2019-11-15)
**Note:** Version bump only for package @ohif/ui
## [0.62.2](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.62.1...@ohif/ui@0.62.2) (2019-11-15)
**Note:** Version bump only for package @ohif/ui
+3 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/ui",
"version": "0.62.2",
"version": "0.64.0",
"description": "A set of React components for Medical Imaging Viewers",
"author": "OHIF Contributors",
"license": "MIT",
@@ -52,7 +52,9 @@
"react-dnd": "9.4.0",
"react-dnd-html5-backend": "^9.4.0",
"react-dnd-touch-backend": "^9.4.0",
"react-draggable": "^4.1.0",
"react-i18next": "^10.11.0",
"react-modal": "^3.11.1",
"react-with-direction": "1.3.0"
},
"devDependencies": {
@@ -1,7 +1,7 @@
import './CineDialog.styl';
import React, { PureComponent } from 'react';
import { withTranslation } from '../../utils/LanguageProvider';
import { withTranslation } from '../../contextProviders';
import { Icon } from './../../elements/Icon';
import PropTypes from 'prop-types';
@@ -58,7 +58,7 @@ const AboutContent = () => {
);
return (
<div data-cy="about-modal">
<div className="AboutContent" data-cy="about-modal">
<div className="btn-group">
<a
className="btn btn-default"
@@ -189,7 +189,7 @@ const ViewportDownloadForm = ({
};
return (
<>
<div className="ViewportDownloadForm">
<div className="title">
{t(
'Please specify the dimensions, filename, and desired type for the output image.'
@@ -284,10 +284,6 @@ const ViewportDownloadForm = ({
className="viewport-preview"
src={viewportPreview.src}
alt="Viewport Preview"
style={{
height: viewportPreview.height,
width: viewportPreview.width,
}}
/>
</div>
@@ -303,7 +299,7 @@ const ViewportDownloadForm = ({
</button>
</div>
</div>
</>
</div>
);
};
@@ -3,15 +3,13 @@
@import '../../../design/styles/common/button.styl'
.ViewportDownloadForm
color: var(--text-secondary-color);
filter: drop-shadow(0 0 3px var(--ui-gray-darkest));
border: none;
border-radius: 8px;
width: inherit;
padding: 15px;
background: transparent;
display: flex;
flex-direction: column;
z-index: 1080 !important;
input, select
max-height: 30px;
.title
margin: 0;
font-weight: bold;
@@ -67,6 +65,7 @@
padding: 10px;
border-radius: 5px;
align-self: center;
margin-bottom: 20px;
@media screen and (max-width: 1023px)
width: 100%;
justify-content: center;
@@ -90,7 +89,6 @@
.actions
display: flex;
height: 60px;
flex-wrap: nowrap;
justify-content: flex-end;
align-items: center;
@@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react';
import i18n from '@ohif/i18n';
import './LanguageSwitcher.styl';
import { withTranslation } from '../../utils/LanguageProvider';
import { withTranslation } from '../../contextProviders';
const LanguageSwitcher = () => {
const getCurrentLanguage = (language = i18n.language) =>
@@ -1,7 +1,7 @@
import './MeasurementTable.styl';
import React, { Component } from 'react';
import { withTranslation } from '../../utils/LanguageProvider';
import { withTranslation } from '../../contextProviders';
import { Icon } from './../../elements/Icon';
import { MeasurementTableItem } from './MeasurementTableItem.js';
@@ -1,6 +1,6 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { withTranslation } from '../../utils/LanguageProvider';
import { withTranslation } from '../../contextProviders';
import { Icon } from './../../elements/Icon';
import { OverlayTrigger } from './../overlayTrigger';
@@ -1,64 +1,69 @@
import React from 'react';
import PropTypes from 'prop-types';
import ReactBootstrapModal from 'react-bootstrap-modal';
import Modal from 'react-modal';
import classNames from 'classnames';
import './OHIFModal.styl';
const customStyle = {
overlay: {
zIndex: 1071,
backgroundColor: 'rgb(0, 0, 0, 0.5)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
},
};
Modal.setAppElement(document.getElementById('root'));
const OHIFModal = ({
className,
closeButton,
backdrop,
keyboard,
show,
shouldCloseOnEsc,
isOpen,
title,
onHide,
footer: Footer,
header: Header,
onClose,
children,
}) => (
<ReactBootstrapModal
className={classNames('modal fade themed in', className)}
backdrop={backdrop}
keyboard={keyboard}
show={show}
large={true}
title={title}
onHide={onHide}
>
{(Header || title) && (
<ReactBootstrapModal.Header closeButton={closeButton}>
{title && (
<ReactBootstrapModal.Title>{title}</ReactBootstrapModal.Title>
)}
{Header && <Header hide={onHide} />}
</ReactBootstrapModal.Header>
)}
<ReactBootstrapModal.Body>{children}</ReactBootstrapModal.Body>
{Footer && (
<ReactBootstrapModal.Footer>
<Footer hide={onHide} />
</ReactBootstrapModal.Footer>
)}
</ReactBootstrapModal>
);
}) => {
const renderHeader = () => {
return (
title && (
<div className="OHIFModal__header">
<h4>{title}</h4>
{closeButton && (
<button data-cy="close-button" onClick={onClose}>
×
</button>
)}
</div>
)
);
};
return (
<Modal
className={classNames('OHIFModal', className)}
shouldCloseOnEsc={shouldCloseOnEsc}
isOpen={isOpen}
title={title}
style={customStyle}
>
<>
{renderHeader()}
<div className="OHIFModal__content">{children}</div>
</>
</Modal>
);
};
OHIFModal.propTypes = {
className: PropTypes.string,
closeButton: PropTypes.bool,
backdrop: PropTypes.bool,
keyboard: PropTypes.bool,
show: PropTypes.bool,
shouldCloseOnEsc: PropTypes.bool,
isOpen: PropTypes.bool,
title: PropTypes.string,
onHide: PropTypes.func,
footer: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node,
PropTypes.func,
]),
header: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node,
PropTypes.func,
]),
onClose: PropTypes.func,
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node,
@@ -0,0 +1,67 @@
.OHIFModal
background-color: var(--ui-gray-darker)
border-color: var(--ui-border-color)
color: var(--text-secondary-color)
border-radius: 6px
border: 0
color: var(--text-primary-color)
position: relative
-webkit-box-shadow: 0 3px 9px rgba(0,0,0,.5)
box-shadow: 0 3px 9px rgba(0,0,0,.5);
background-clip: padding-box
outline: 0
@media (min-width: 320px)
width: 78%
min-width: 300px
@media (min-width: 768px)
width: 600px
@media (min-width: 992px)
width: 900px
&__content
padding: 20px
max-height: 90vh;
overflow-x: hidden;
overflow-y: auto;
scrollbar-width: none;
-ms-overflow-style: none;
&::-webkit-scrollbar
display: none;
&__header
display: flex
justify-content: space-between
align-items: center
border-bottom-width: 3px
border-bottom-style: solid
border-bottom-color: #000000
padding: 20px
position: relative
h4
color: var(--text-secondary-color)
font-size: 20px
font-weight: 500
line-height: 24px
padding-right: 24px
margin: 0
button
cursor: pointer
margin: -10px 0 0 0
padding: 0
background-color: transparent
border: none
color: var(--text-secondary-color)
font-size: 25px
font-weight: lighter
&:active,
&:focus,
&:focus:active
background-image: none
outline: 0
box-shadow: none
@@ -3,7 +3,7 @@ import React, { cloneElement } from 'react';
import PropTypes from 'prop-types';
import { Overlay as BaseOverlay } from 'react-overlays';
import elementType from 'prop-types-extra/lib/elementType';
import { withTranslation } from '../../utils/LanguageProvider';
import { withTranslation } from '../../contextProviders';
import Fade from './Fade';
@@ -1,7 +1,7 @@
import React from 'react';
import SnackbarItem from './SnackbarItem';
import './Snackbar.css';
import { useSnackbarContext } from '../../utils/SnackbarProvider';
import { useSnackbarContext } from '../../contextProviders';
const SnackbarContainer = () => {
const { snackbarItems, hide } = useSnackbarContext();
@@ -37,8 +37,8 @@ const SnackbarContainer = () => {
return (
<div key={pos} className={`sb-container sb-${pos}`}>
{items[pos].map(item => (
<div key={item.id}>{renderItem(item)}</div>
{items[pos].map((item, index) => (
<div key={item.id + index}>{renderItem(item)}</div>
))}
</div>
);
@@ -102,14 +102,14 @@ function ImageThumbnail(props) {
{shouldRenderToCanvas() ? (
<canvas ref={canvasRef} width={width} height={height} />
) : (
<img
className="static-image"
src={imageSrc}
//width={this.props.width}
height={height}
alt={''}
/>
)}
<img
className="static-image"
src={imageSrc}
//width={this.props.width}
height={height}
alt={''}
/>
)}
</div>
{loadingOrError}
{showStackLoadingProgressBar && (
@@ -2,22 +2,23 @@
/* Will edit everything selected including everything between a range of dates */
.CalendarDay__selected_span {
background: var(--table-text-secondary-color);
color: #fff;
background: var(--datePicker-picker-background);
color: var(--datePicker-picker-text);
border-color: #e4e7e7;
}
/* Will edit selected date or the endpoints of a range of dates */
.CalendarDay__selected {
background: var(--table-text-secondary-color);
color: #fff;
background: var(--datePicker-picker-background);
color: var(--datePicker-picker-text);
border-color: #e4e7e7;
}
/* Will edit when hovered over. _span style also has this property */
.CalendarDay__selected:hover {
background: var(--table-text-secondary-color);
color: #fff;
.CalendarDay__selected:hover,
.CalendarDay__default:hover {
background: var(--datePicker-picker-background);
color: var(--datePicker-picker-text);
border-color: #e4e7e7;
}
@@ -25,8 +26,8 @@
is not yet selected. Edits the dates between your mouse and said date */
.CalendarDay__hovered_span:hover,
.CalendarDay__hovered_span {
background: var(--table-text-secondary-color);
color: #fff;
background: var(--datePicker-picker-background);
color: var(--datePicker-picker-text);
border-color: #e4e7e7;
}
@@ -42,15 +43,15 @@ is not yet selected. Edits the dates between your mouse and said date */
/* Container - visual */
.DateRangePickerInput {
width: calc(100% - 10px); /* Just use padding? */
background-color: var(--input-background-color);
border-color: var(--input-background-color);
color: var(--input-placeholder-color);
background-color: var(--datePicker-input-background);
border-color: var(--datePicker-input-background);
color: var(--datePicker-picker-text-input);
height: 40px;
}
.DateRangePickerInput.DateRangePickerInput__withBorder {
border-radius: 4px;
background-color: var(--input-background-color);
background-color: var(--datePicker-input-background);
}
/* Input Container */
@@ -58,14 +59,14 @@ is not yet selected. Edits the dates between your mouse and said date */
width: 97px;
height: 38px;
border-radius: 4px;
background-color: var(--input-background-color);
background-color: var(--datePicker-input-background);
}
/* Actual Input Element */
.DateInput > .DateInput_input {
border-color: transparent;
background-color: transparent;
color: var(--input-placeholder-color);
color: var(--datePicker-picker-text-input);
height: 38px;
font-size: 10pt;
padding: 0;
@@ -80,4 +81,39 @@ is not yet selected. Edits the dates between your mouse and said date */
.PresetDateRangePicker_button {
margin: 0;
padding: 4px 8px;
color: #00a699;
background: var(--datePicker-picker-text-input);
}
.PresetDateRangePicker_button:hover {
background: #00a699;
color: var(--datePicker-picker-text-input);
}
.CalendarDay__blocked_out_of_range:hover {
background: #fff;
cursor: not-allowed;
color: var(--datePicker-picker-text-disabled);
}
.DateRangePicker_select {
background-color: white;
color: var(--datePicker-picker-text);
border: 1px solid #e4e7e7;
border-radius: 4px;
display: inline-block;
padding: 3px 20px 3px 10px;
font-size: 14px;
cursor: pointer;
appearance: none;
background-image: linear-gradient(45deg, transparent 50%, gray 50%),
linear-gradient(135deg, gray 50%, transparent 50%);
background-position: calc(100% - 11px) 11px, calc(100% - 6px) calc(11px);
background-size: 5px 5px, 5px 5px;
background-repeat: no-repeat;
}
.DateRangePicker_select:hover {
border-color: #c4c4c4;
}
@@ -7,39 +7,25 @@ import './CustomDateRangePicker.css';
import React from 'react';
import PropTypes from 'prop-types';
import { DateRangePicker } from 'react-dates';
import moment from 'moment';
import i18n from '@ohif/i18n';
import { useTranslation } from 'react-i18next';
export default class CustomDateRangePicker extends React.Component {
static propTypes = {
presets: PropTypes.arrayOf(
PropTypes.shape({
text: PropTypes.string,
start: PropTypes.required,
end: PropTypes.required,
})
),
};
function CustomDateRangePicker(props) {
moment.locale(i18n.language); // using i18n in the date picker
constructor(props) {
super(props);
this.state = {
startDate: this.props.startDate,
endDate: this.props.endDate,
};
const { t } = useTranslation('DatePicker');
this.renderDatePresets = this.renderDatePresets.bind(this);
this.onDatesChange = this.onDatesChange.bind(this);
}
const {
onDatesChange,
startDate,
endDate,
presets,
...dateRangePickerProps
} = props;
onDatesChange({ startDate, endDate, preset }) {
this.setState({ startDate, endDate });
if (this.props.onDatesChange) {
this.props.onDatesChange({ startDate, endDate, preset });
}
}
renderDatePresets() {
const { presets } = this.props;
const { startDate, endDate } = this.state;
const renderDatePresets = () => {
const { presets } = props;
return (
<div className="PresetDateRangePicker_panel">
@@ -54,7 +40,7 @@ export default class CustomDateRangePicker extends React.Component {
isSelected ? 'PresetDateRangePicker_button__selected' : ''
}`}
onClick={() =>
this.onDatesChange({
onDatesChange({
startDate: start,
endDate: end,
preset: true,
@@ -67,30 +53,89 @@ export default class CustomDateRangePicker extends React.Component {
})}
</div>
);
}
};
const renderMonthElement = ({ month, onMonthSelect, onYearSelect }) => {
const containerStyle = {
margin: '0 5px',
};
render() {
let {
autoFocus,
autoFocusEndDate,
initialStartDate,
initialEndDate,
stateDateWrapper,
onDatesChange,
startDate,
endDate,
presets,
...dateRangePickerProps
} = this.props;
const renderYearsOptions = () => {
const yearsRange = 20;
const options = [];
for (let i = 0; i < yearsRange; i++) {
const year = moment().year() - i;
options.push(<option value={year}>{year}</option>);
}
return options;
};
renderMonthElement.propTypes = {
onMonthSelect: PropTypes.func,
onYearSelect: PropTypes.func,
};
return (
<DateRangePicker
{...dateRangePickerProps}
startDate={this.state.startDate}
endDate={this.state.endDate}
renderCalendarInfo={this.renderDatePresets}
onDatesChange={this.onDatesChange}
/>
<div style={{ display: 'flex', justifyContent: 'center' }}>
<div style={containerStyle}>
<select
className="DateRangePicker_select"
value={month.month()}
onChange={e => onMonthSelect(month, e.target.value)}
>
{moment.months().map((label, value) => (
<option key={value} value={value}>
{label}
</option>
))}
</select>
</div>
<div style={containerStyle}>
{}
<select
className="DateRangePicker_select"
value={month.year()}
onChange={e => onYearSelect(month, e.target.value)}
>
{renderYearsOptions()}
</select>
</div>
</div>
);
}
};
return (
<DateRangePicker
{...dateRangePickerProps}
startDate={startDate}
endDate={endDate}
renderCalendarInfo={renderDatePresets}
onDatesChange={onDatesChange}
renderMonthElement={renderMonthElement}
startDatePlaceholderText={t('Start Date')}
endDatePlaceholderText={t('End Date')}
phrases={{
closeDatePicker: t('Common:Close'),
clearDates: t('Clear dates'),
}}
/>
);
}
CustomDateRangePicker.propTypes = {
presets: PropTypes.arrayOf(
PropTypes.shape({
text: PropTypes.string,
start: PropTypes.required,
end: PropTypes.required,
})
),
autoFocus: PropTypes.bool.isRequired,
onDatesChange: PropTypes.func.isRequired,
startDate: PropTypes.instanceOf(Date),
endDate: PropTypes.instanceOf(Date),
month: PropTypes.instanceOf(Date),
};
export default CustomDateRangePicker;
@@ -6,7 +6,7 @@ import TableSearchFilter from './TableSearchFilter.js';
import useMedia from '../../hooks/useMedia.js';
import PropTypes from 'prop-types';
import { StudyListLoadingText } from './StudyListLoadingText.js';
import { withTranslation } from '../../utils/LanguageProvider';
import { withTranslation } from '../../contextProviders';
/**
*
@@ -25,7 +25,6 @@ function StudyList(props) {
onFilterChange: handleFilterChange,
onSelectItem: handleSelectItem,
t,
//
studyListDateFilterNumDays,
} = props;
@@ -208,7 +207,6 @@ StudyList.propTypes = {
allFields: PropTypes.string.isRequired,
}).isRequired,
onFilterChange: PropTypes.func.isRequired,
//
studyListDateFilterNumDays: PropTypes.number,
};
@@ -176,7 +176,7 @@ table.table
margin: 0 auto;
color: var(--table-text-primary-color);
font-weight: 400;
padding: 20px;
padding: 20px 8px;
user-select: none;
font-size: 15px;
@@ -1,7 +1,7 @@
import React from 'react';
import { Icon } from './../../elements/Icon';
// TODO: useTranslation
import { withTranslation } from '../../utils/LanguageProvider';
import { withTranslation } from '../../contextProviders';
function StudyListLoadingText({ t: translate }) {
return (
@@ -1,7 +1,7 @@
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import './PaginationArea.styl';
import { withTranslation } from '../../utils/LanguageProvider';
import { withTranslation } from '../../contextProviders';
class TablePagination extends PureComponent {
static defaultProps = {
@@ -6,8 +6,6 @@ import CustomDateRangePicker from './CustomDateRangePicker.js';
import { Icon } from './../../elements/Icon';
import { useTranslation } from 'react-i18next';
function TableSearchFilter(props) {
const {
meta,
@@ -17,7 +15,7 @@ function TableSearchFilter(props) {
sortFieldName,
sortDirection,
// TODO: Rename
studyListDateFilterNumDays
studyListDateFilterNumDays,
} = props;
const [focusedInput, setFocusedInput] = useState(null);
const [t] = useTranslation(); // 'Common'?
@@ -25,11 +23,13 @@ function TableSearchFilter(props) {
const sortIcons = ['sort', 'sort-up', 'sort-down'];
const sortIconForSortField =
sortDirection === 'asc' ? sortIcons[1] : sortIcons[2];
//
const today = moment();
const lastWeek = moment().subtract(7, 'day');
const lastMonth = moment().subtract(1, 'month');
const defaultStartDate = moment().subtract(studyListDateFilterNumDays, 'days');
const defaultStartDate = moment().subtract(
studyListDateFilterNumDays,
'days'
);
const defaultEndDate = today;
const studyDatePresets = [
{
@@ -82,8 +82,8 @@ function TableSearchFilter(props) {
endDateId="end-date"
// TODO: We need a dynamic way to determine which fields values to update
onDatesChange={({ startDate, endDate, preset = false }) => {
onValueChange('studyDateTo', startDate);
onValueChange('studyDateFrom', endDate);
onValueChange('studyDateFrom', startDate);
onValueChange('studyDateTo', endDate);
}}
focusedInput={focusedInput}
onFocusChange={updatedVal => setFocusedInput(updatedVal)}
@@ -1,7 +1,5 @@
.HotKeysPreferences
display: flex;
margin-right: -15px;
margin-left: -15px;
.column
width: 50%;
@@ -90,8 +90,8 @@ export class UserPreferences extends Component {
render() {
return (
<div>
<div className="dialog-separator-after">
<div className="UserPreferences">
<div className="UserPreferences__selector">
<ul className="nav nav-tabs">
<li
onClick={() => {
@@ -3,23 +3,27 @@
@import './../../design/styles/common/state.styl'
@import './../../design/styles/common/global.styl'
.modal-body
overflow: hidden
.UserPreferences
display: flex
flex-direction: column
.errorMessage
color: var(--state-error-text)
font-size: 10px
text-transform: uppercase;
&__selector
border-bottom: 3px solid black
.form-content
border-bottom: 3px solid var(--primary-background-color)
margin-bottom: 20px
margin-left: -22px
margin-right: -22px
max-height: 70vh
overflow-y: auto
padding: 22px
min-height: 500px
.errorMessage
color: var(--state-error-text)
font-size: 10px
text-transform: uppercase;
.popover
width: 300px
.form-content
border-bottom: 3px solid var(--primary-background-color)
margin-bottom: 20px
margin-left: -20px
margin-right: -20px
max-height: 70vh
overflow-y: auto
padding: 20px
min-height: 500px
.popover
width: 300px
@@ -2,7 +2,7 @@ import './UserPreferencesForm.styl';
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { withTranslation } from '../../utils/LanguageProvider';
import { withTranslation } from '../../contextProviders';
import cloneDeep from 'lodash.clonedeep';
import isEqual from 'lodash.isequal';
@@ -16,7 +16,6 @@
.footer
display: flex
flex-direction: row
padding-bottom: 20px
justify-content: space-between
div
@@ -0,0 +1,208 @@
import React, {
useState,
createContext,
useContext,
useCallback,
useEffect,
} from 'react';
import PropTypes from 'prop-types';
import Draggable from 'react-draggable';
import classNames from 'classnames';
import { utils } from '@ohif/core';
import './DialogProvider.styl';
const DialogContext = createContext(null);
export const useDialog = () => useContext(DialogContext);
const DialogProvider = ({ children, service }) => {
const [isDragging, setIsDragging] = useState(false);
const [dialogs, setDialogs] = useState([]);
const [lastDialogPosition, setLastDialogPosition] = useState(null);
/**
* Sets the implementation of a dialog service that can be used by extensions.
*
* @returns void
*/
useEffect(() => {
if (service) {
service.setServiceImplementation({ create, dismiss, dismissAll });
}
}, [create, dismiss, service]);
/**
* Creates a dialog and return its id.
*
* @returns id
*/
const create = useCallback(
({
id,
content,
onSubmit,
onClose,
onDrag,
onStop,
isDraggable,
defaultPosition,
position,
}) => {
let dialogId = id;
if (!dialogId) {
dialogId = utils.guid();
}
const newDialog = {
id: dialogId,
content,
onSubmit,
onClose,
onDrag,
onStop,
isDraggable,
defaultPosition,
position,
};
setDialogs(dialogs => [...dialogs, newDialog]);
return dialogId;
},
[]
);
/**
* Dismisses the dialog with a given id.
*
* @returns void
*/
const dismiss = useCallback(({ id }) => {
setDialogs(dialogs => dialogs.filter(dialog => dialog.id !== id));
}, []);
/**
* Dismisses all dialogs.
*
* @returns void
*/
const dismissAll = () => {
setDialogs([]);
};
/**
* Moves the dialog to the foreground if clicked.
*
* @returns void
*/
const _reorder = id => {
setDialogs(dialogs => [
...dialogs.filter(dialog => dialog.id !== id),
dialogs.find(dialog => dialog.id === id),
]);
};
const _updateLastDialogPosition = dialogId => {
const draggableItemBounds = document
.querySelector(`#draggableItem-${dialogId}`)
.getBoundingClientRect();
setLastDialogPosition({
x: draggableItemBounds.x,
y: draggableItemBounds.y,
});
};
return (
<DialogContext.Provider value={{ create, dismiss, dismissAll, dialogs }}>
<div className="DraggableArea">
{dialogs.map(dialog => {
const {
id,
content: Dialog,
position /* Position of the dialog. {{x: 0, y: 0}} */,
defaultPosition,
isDraggable = true,
onStart = () => {},
onStop = () => {},
onDrag = () => {},
} = dialog;
return (
<Draggable
key={id}
disabled={!isDraggable}
position={position}
defaultPosition={lastDialogPosition || defaultPosition}
bounds="parent"
onStart={event => {
const e = event || window.event;
const target = e.target || e.srcElement;
const BLACKLIST = ['SVG', 'BUTTON', 'PATH', 'INPUT'];
if (BLACKLIST.includes(target.tagName.toUpperCase())) {
return false;
}
onStart(event);
}}
onStop={event => {
onStop(event);
setIsDragging(false);
return;
}}
onDrag={event => {
setIsDragging(true);
_reorder(id);
_updateLastDialogPosition(id);
onDrag(event);
}}
>
<div
id={`draggableItem-${id}`}
className={classNames(
'DraggableItem',
isDragging && 'dragging'
)}
style={{ zIndex: '999', position: 'absolute' }}
onClick={() => _reorder(id)}
>
<Dialog {...dialog} />
</div>
</Draggable>
);
})}
</div>
{children}
</DialogContext.Provider>
);
};
DialogProvider.defaultProps = {
service: null,
};
DialogProvider.propTypes = {
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node,
PropTypes.func,
]).isRequired,
service: PropTypes.shape({
setServiceImplementation: PropTypes.func,
}),
};
/**
*
* High Order Component to use the dialog methods through a Class Component
*
*/
export const withDialog = Component => {
return function WrappedComponent(props) {
const { create, dismiss, dismissAll } = useDialog();
return <Component {...props} dialog={{ create, dismiss, dismissAll }} />;
};
};
export default DialogProvider;
@@ -0,0 +1,12 @@
.DraggableItem
div
cursor: grab !important
.DraggableItem.dragging
div
cursor: grabbing !important
.DraggableArea
width: 100vw
height: 100vh
position: absolute
@@ -16,14 +16,11 @@ export const useModal = () => useContext(ModalContext);
const ModalProvider = ({ children, modal: Modal, service }) => {
const DEFAULT_OPTIONS = {
component: null /* The component instance inside the modal. */,
header: null /* The content inside the modal header. */,
footer: null /* The content inside the modal footer. */,
backdrop: false /* Should the modal render a backdrop overlay. */,
keyboard: false /* Modal is dismissible via the esc key. */,
show: true /* Make the Modal visible or hidden. */,
shouldCloseOnEsc: false /* Modal is dismissible via the esc key. */,
isOpen: true /* Make the Modal visible or hidden. */,
closeButton: true /* Should the modal body render the close button. */,
title: null /* Should the modal render the title independently of the body content. */,
customClassName: null /* The custom class to style the modal. */,
customClassName: '' /* The custom class to style the modal. */,
};
const [options, setOptions] = useState(DEFAULT_OPTIONS);
@@ -69,14 +66,11 @@ const ModalProvider = ({ children, modal: Modal, service }) => {
options.customClassName,
options.component.className
)}
backdrop={options.backdrop}
keyboard={options.keyboard}
show={options.show}
shouldCloseOnEsc={options.keyboard}
isOpen={options.isOpen}
title={options.title}
closeButton={options.closeButton}
footer={options.footer}
header={options.header}
onHide={hide}
onClose={hide}
>
<Component {...options} show={show} hide={hide} />
</Modal>
+20
View File
@@ -0,0 +1,20 @@
export {
default as ModalProvider,
useModal,
withModal,
ModalConsumer,
} from './ModalProvider.js';
export {
default as SnackbarProvider,
useSnackbarContext,
withSnackbar,
} from './SnackbarProvider.js';
export {
default as LanguageProvider,
withTranslation,
} from './LanguageProvider.js';
export {
default as DialogProvider,
withDialog,
useDialog,
} from './DialogProvider.js';
+11 -5
View File
@@ -49,15 +49,18 @@ import { ScrollableArea } from './ScrollableArea/ScrollableArea.js';
import Toolbar from './viewer/Toolbar.js';
import ToolbarButton from './viewer/ToolbarButton.js';
import ViewerbaseDragDropContext from './utils/viewerbaseDragDropContext.js';
import SnackbarProvider, {
import {
SnackbarProvider,
useSnackbarContext,
withSnackbar,
} from './utils/SnackbarProvider';
import ModalProvider, {
DialogProvider,
useDialog,
withDialog,
ModalProvider,
ModalConsumer,
useModal,
withModal,
ModalConsumer,
} from './utils/ModalProvider';
} from './contextProviders';
export {
// Elements
@@ -111,6 +114,9 @@ export {
ModalConsumer,
withModal,
OHIFModal,
DialogProvider,
withDialog,
useDialog,
// Hooks
useDebounce,
useMedia,
+1 -1
View File
@@ -4,7 +4,7 @@ import { Icon } from './../elements/Icon';
import PropTypes from 'prop-types';
import React from 'react';
import classnames from 'classnames';
import { withTranslation } from '../utils/LanguageProvider';
import { withTranslation } from '../contextProviders';
export function ToolbarButton(props) {
const { isActive, icon, labelWhenActive, onClick, t } = props;
+41
View File
@@ -3,6 +3,47 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [2.10.0](https://github.com/OHIF/Viewers/compare/@ohif/viewer@2.9.0...@ohif/viewer@2.10.0) (2019-11-19)
### Features
* New dialog service ([#1202](https://github.com/OHIF/Viewers/issues/1202)) ([f65639c](https://github.com/OHIF/Viewers/commit/f65639c2b0dab01decd20cab2cef4263cb4fab37))
# [2.9.0](https://github.com/OHIF/Viewers/compare/@ohif/viewer@2.8.5...@ohif/viewer@2.9.0) (2019-11-19)
### Features
* Issue 879 viewer route query param not filtering but promoting ([#1141](https://github.com/OHIF/Viewers/issues/1141)) ([b17f753](https://github.com/OHIF/Viewers/commit/b17f753e6222045252ef885e40233681541a32e1)), closes [#1118](https://github.com/OHIF/Viewers/issues/1118)
## [2.8.5](https://github.com/OHIF/Viewers/compare/@ohif/viewer@2.8.4...@ohif/viewer@2.8.5) (2019-11-18)
### Bug Fixes
* minor date picker UX improvements ([813ee5e](https://github.com/OHIF/Viewers/commit/813ee5ed4d78b7bda234922d5f3389efe346451c))
## [2.8.4](https://github.com/OHIF/Viewers/compare/@ohif/viewer@2.8.3...@ohif/viewer@2.8.4) (2019-11-15)
**Note:** Version bump only for package @ohif/viewer
## [2.8.3](https://github.com/OHIF/Viewers/compare/@ohif/viewer@2.8.2...@ohif/viewer@2.8.3) (2019-11-15)
**Note:** Version bump only for package @ohif/viewer
@@ -1,14 +1,14 @@
//We are keeping the hardcoded results values for the study list tests
//this is intended to be running in a controled docker environment with test data.
describe('OHIF Study List', function() {
context('Desktop resolution', function() {
beforeEach(function() {
describe('OHIF Study List', function () {
context('Desktop resolution', function () {
beforeEach(function () {
cy.viewport(1750, 720);
cy.openStudyList();
cy.initStudyListAliasesOnDesktop();
});
it('searches Patient Name with exact string', function() {
it('searches Patient Name with exact string', function () {
cy.get('@patientName').type('Juno');
//Wait result list to be displayed
cy.waitStudyList();
@@ -18,7 +18,7 @@ describe('OHIF Study List', function() {
});
});
it('searches MRN with exact string', function() {
it('searches MRN with exact string', function () {
cy.get('@MRN').type('ProstateX-0000');
//Wait result list to be displayed
cy.waitStudyList();
@@ -28,7 +28,7 @@ describe('OHIF Study List', function() {
});
});
it('searches Accession with exact string', function() {
it('searches Accession with exact string', function () {
cy.get('@accessionNumber').type('fpcben98890');
//Wait result list to be displayed
cy.waitStudyList();
@@ -38,7 +38,7 @@ describe('OHIF Study List', function() {
});
});
it('searches Modality with camel case', function() {
it('searches Modality with camel case', function () {
cy.get('@modalities').type('Mr');
//Wait result list to be displayed
cy.waitStudyList();
@@ -48,7 +48,7 @@ describe('OHIF Study List', function() {
});
});
it('searches Description with exact string', function() {
it('searches Description with exact string', function () {
cy.get('@studyDescription').type('CHEST');
//Wait result list to be displayed
cy.waitStudyList();
@@ -58,7 +58,7 @@ describe('OHIF Study List', function() {
});
});
it('changes rows per page and checks the study count', function() {
it('changes rows per page and checks the study count', function () {
//Show rows per page options
const pageRows = [25, 50, 100];
@@ -100,14 +100,14 @@ describe('OHIF Study List', function() {
// });
});
context('Tablet resolution', function() {
beforeEach(function() {
context('Tablet resolution', function () {
beforeEach(function () {
cy.viewport(1000, 660);
cy.openStudyList();
cy.initStudyListAliasesOnTablet();
});
it('searches Patient Name with exact string', function() {
it('searches Patient Name with exact string', function () {
cy.get('@patientNameOrMRN').type('Juno');
//Wait result list to be displayed
cy.waitStudyList();
@@ -117,7 +117,7 @@ describe('OHIF Study List', function() {
});
});
it('searches MRN with with exact string', function() {
it('searches MRN with with exact string', function () {
cy.get('@patientNameOrMRN').type('ProstateX-0000');
//Wait result list to be displayed
cy.waitStudyList();
@@ -127,7 +127,7 @@ describe('OHIF Study List', function() {
});
});
it('searches Modality with exact string', function() {
it('searches Modality with exact string', function () {
cy.get('@accessionModalityDescription').type('MR');
//Wait result list to be displayed
cy.waitStudyList();
@@ -137,7 +137,7 @@ describe('OHIF Study List', function() {
});
});
it('searches Accession with exact string', function() {
it('searches Accession with exact string', function () {
cy.get('@accessionModalityDescription').type('fpcben98890');
//Wait result list to be displayed
cy.waitStudyList();
@@ -147,7 +147,7 @@ describe('OHIF Study List', function() {
});
});
it('searches Description with exact string', function() {
it('searches Description with exact string', function () {
cy.get('@accessionModalityDescription').type('CHEST');
//Wait result list to be displayed
cy.waitStudyList();
@@ -157,7 +157,7 @@ describe('OHIF Study List', function() {
});
});
it('changes rows per page and checks the study count', function() {
it('changes rows per page and checks the study count', function () {
//Show rows per page options
const pageRows = [25, 50, 100];
@@ -273,7 +273,7 @@ describe('OHIF Study Viewer Page', function() {
cy.get('[data-cy="about-item-menu"]')
.first()
.click();
cy.get('.modal-content')
cy.get('[data-cy="about-modal"]')
.as('aboutOverlay')
.should('be.visible');
@@ -296,7 +296,7 @@ describe('OHIF Study Viewer Page', function() {
cy.percyCanvasSnapshot('About modal - Should display modal');
//close modal
cy.get('.close').click();
cy.get('[data-cy="close-button"]').click();
cy.get('@aboutOverlay').should('not.be.enabled');
});
});
+5 -5
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/viewer",
"version": "2.8.3",
"version": "2.10.0",
"description": "OHIF Viewer",
"author": "OHIF Contributors",
"license": "MIT",
@@ -45,14 +45,14 @@
},
"dependencies": {
"@babel/runtime": "^7.5.5",
"@ohif/core": "^1.9.1",
"@ohif/core": "^1.12.0",
"@ohif/extension-cornerstone": "^2.0.0",
"@ohif/extension-dicom-html": "^1.0.1",
"@ohif/extension-dicom-microscopy": "^0.50.6",
"@ohif/extension-dicom-pdf": "^1.0.0",
"@ohif/extension-vtk": "^0.53.7",
"@ohif/i18n": "^0.52.0",
"@ohif/ui": "^0.62.2",
"@ohif/extension-vtk": "^0.53.11",
"@ohif/i18n": "^0.52.1",
"@ohif/ui": "^0.64.0",
"@tanem/react-nprogress": "^1.1.25",
"classnames": "^2.2.6",
"core-js": "^3.2.1",
+1
View File
@@ -3,6 +3,7 @@ window.config = {
routerBasename: '/',
extensions: [],
showStudyList: true,
filterQueryParam: false,
servers: {
dicomWeb: [
{
+51 -31
View File
@@ -1,7 +1,18 @@
import React, { Component } from 'react';
import { OidcProvider } from 'redux-oidc';
import { I18nextProvider } from 'react-i18next';
import PropTypes from 'prop-types';
import { Provider } from 'react-redux';
import { BrowserRouter as Router } from 'react-router-dom';
import OHIFCornerstoneExtension from '@ohif/extension-cornerstone';
import { hot } from 'react-hot-loader/root';
// TODO: This should not be here
import './config';
import {
SnackbarProvider,
ModalProvider,
DialogProvider,
OHIFModal,
} from '@ohif/ui';
import {
CommandsManager,
@@ -10,44 +21,48 @@ import {
HotkeysManager,
createUINotificationService,
createUIModalService,
createUIDialogService,
utils,
} from '@ohif/core';
import React, { Component } from 'react';
import i18n from '@ohif/i18n';
// TODO: This should not be here
import './config';
/** Utils */
import {
getUserManagerForOpenIdConnectClient,
initWebWorkers,
} from './utils/index.js';
import { I18nextProvider } from 'react-i18next';
// ~~ EXTENSIONS
/** Extensions */
import { GenericViewerCommands, MeasurementsPanel } from './appExtensions';
import OHIFCornerstoneExtension from '@ohif/extension-cornerstone';
import OHIFStandaloneViewer from './OHIFStandaloneViewer';
import { OidcProvider } from 'redux-oidc';
import PropTypes from 'prop-types';
import { Provider } from 'react-redux';
import { BrowserRouter as Router } from 'react-router-dom';
import { getActiveContexts } from './store/layout/selectors.js';
import i18n from '@ohif/i18n';
import store from './store';
import { SnackbarProvider, ModalProvider, OHIFModal } from '@ohif/ui';
// Contexts
/** Viewer */
import OHIFStandaloneViewer from './OHIFStandaloneViewer';
/** Store */
import { getActiveContexts } from './store/layout/selectors.js';
import store from './store';
/** Contexts */
import WhiteLabellingContext from './context/WhiteLabellingContext';
import UserManagerContext from './context/UserManagerContext';
import AppContext from './context/AppContext';
// ~~~~ APP SETUP
/** ~~~~~~~~~~~~~ Application Setup */
const commandsManagerConfig = {
getAppState: () => store.getState(),
getActiveContexts: () => getActiveContexts(store.getState()),
};
// Services
/** Services */
const UINotificationService = createUINotificationService();
const UIModalService = createUIModalService();
const UIDialogService = createUIDialogService();
/** Managers */
const commandsManager = new CommandsManager(commandsManagerConfig);
const hotkeysManager = new HotkeysManager(commandsManager);
const servicesManager = new ServicesManager();
@@ -55,7 +70,7 @@ const extensionManager = new ExtensionManager({
commandsManager,
servicesManager,
});
// ~~~~ END APP SETUP
/** ~~~~~~~~~~~~~ End Application Setup */
// TODO[react] Use a provider when the whole tree is React
window.store = store;
@@ -72,6 +87,7 @@ class App extends Component {
id: PropTypes.string.isRequired,
})
),
hotkeys: PropTypes.array,
};
static defaultProps = {
@@ -91,7 +107,7 @@ class App extends Component {
const { servers, extensions, hotkeys, oidc } = props;
this.initUserManager(oidc);
_initServices([UINotificationService, UIModalService]);
_initServices([UINotificationService, UIModalService, UIDialogService]);
_initExtensions(extensions, hotkeys);
_initServers(servers);
initWebWorkers();
@@ -114,12 +130,14 @@ class App extends Component {
<Router basename={routerBasename}>
<WhiteLabellingContext.Provider value={whiteLabelling}>
<SnackbarProvider service={UINotificationService}>
<ModalProvider
modal={OHIFModal}
service={UIModalService}
>
<OHIFStandaloneViewer userManager={userManager} />
</ModalProvider>
<DialogProvider service={UIDialogService}>
<ModalProvider
modal={OHIFModal}
service={UIModalService}
>
<OHIFStandaloneViewer userManager={userManager} />
</ModalProvider>
</DialogProvider>
</SnackbarProvider>
</WhiteLabellingContext.Provider>
</Router>
@@ -138,9 +156,11 @@ class App extends Component {
<Router basename={routerBasename}>
<WhiteLabellingContext.Provider value={whiteLabelling}>
<SnackbarProvider service={UINotificationService}>
<ModalProvider modal={OHIFModal} service={UIModalService}>
<OHIFStandaloneViewer />
</ModalProvider>
<DialogProvider service={UIDialogService}>
<ModalProvider modal={OHIFModal} service={UIModalService}>
<OHIFStandaloneViewer />
</ModalProvider>
</DialogProvider>
</SnackbarProvider>
</WhiteLabellingContext.Provider>
</Router>
@@ -185,7 +205,7 @@ class App extends Component {
}
function _initServices(services) {
services.forEach(service => servicesManager.register(service));
servicesManager.registerServices(services);
}
/**
@@ -10,7 +10,7 @@ export default {
preRegistration({ servicesManager, configuration = {} }) {
init({ servicesManager, configuration });
},
getPanelModule() {
getPanelModule({ servicesManager }) {
return {
menuOptions: [
{
@@ -49,7 +49,6 @@ class Header extends Component {
onClick: () =>
show(AboutContent, {
title: t('OHIF Viewer - About'),
customClassName: 'AboutContent',
}),
},
{
@@ -1,6 +1,10 @@
import { connect } from 'react-redux';
import ViewerRetrieveStudyData from './ViewerRetrieveStudyData.js';
import OHIF from "@ohif/core";
const {
clearViewportSpecificData
} = OHIF.redux.actions;
const isActive = a => a.active === true;
const mapStateToProps = state => {
@@ -10,10 +14,17 @@ const mapStateToProps = state => {
server: activeServer,
};
};
const mapDispatchToProps = dispatch => {
return {
clearViewportSpecificData: () => {
dispatch(clearViewportSpecificData());
}
};
};
const ConnectedViewerRetrieveStudyData = connect(
mapStateToProps,
null
mapDispatchToProps
)(ViewerRetrieveStudyData);
export default ConnectedViewerRetrieveStudyData;
@@ -7,6 +7,7 @@ import {
RoundedButtonGroup,
ToolbarButton,
withModal,
withDialog,
} from '@ohif/ui';
import './ToolbarRow.css';
@@ -45,7 +46,6 @@ class ToolbarRow extends Component {
this.state = {
toolbarButtons: toolbarButtonDefinitions,
activeButtons: [],
isCineDialogOpen: false,
};
this._handleBuiltIn = _handleBuiltIn.bind(this);
@@ -106,13 +106,6 @@ class ToolbarRow extends Component {
this.state.activeButtons
);
const cineDialogContainerStyle = {
display: this.state.isCineDialogOpen ? 'block' : 'none',
position: 'absolute',
top: '82px',
zIndex: 999,
};
const onPress = (side, value) => {
this.props.handleSidePanelChange(side, value);
};
@@ -145,9 +138,6 @@ class ToolbarRow extends Component {
)}
</div>
</div>
<div className="CineDialogContainer" style={cineDialogContainerStyle}>
<ConnectedCineDialog />
</div>
</>
);
}
@@ -160,7 +150,8 @@ function _getCustomButtonComponent(button, activeButtons) {
// Check if its a valid customComponent. Later on an CustomToolbarComponent interface could be implemented.
if (isValidComponent) {
const parentContext = this;
const isActive = activeButtons.includes(button.id);
const activeButtonsIds = activeButtons.map(button => button.id);
const isActive = activeButtonsIds.includes(button.id);
return (
<CustomComponent
@@ -168,7 +159,7 @@ function _getCustomButtonComponent(button, activeButtons) {
toolbarClickCallback={_handleToolbarButtonClick.bind(this)}
button={button}
key={button.id}
activeButtons={activeButtons}
activeButtons={activeButtonsIds}
isActive={isActive}
/>
);
@@ -181,7 +172,7 @@ function _getExpandableButtonComponent(button, activeButtons) {
const childButtons = button.buttons.map(childButton => {
childButton.onClick = _handleToolbarButtonClick.bind(this, childButton);
if (activeButtons.indexOf(childButton.id) > -1) {
if (activeButtons.map(button => button.id).indexOf(childButton.id) > -1) {
activeCommand = childButton.id;
}
@@ -206,7 +197,7 @@ function _getDefaultButtonComponent(button, activeButtons) {
label={button.label}
icon={button.icon}
onClick={_handleToolbarButtonClick.bind(this, button)}
isActive={activeButtons.includes(button.id)}
isActive={activeButtons.map(button => button.id).includes(button.id)}
/>
);
}
@@ -241,6 +232,8 @@ function _getButtonComponents(toolbarButtons, activeButtons) {
* @param {*} props
*/
function _handleToolbarButtonClick(button, evt, props) {
const { activeButtons } = this.state;
if (button.commandName) {
const options = Object.assign({ evt }, button.commandOptions);
commandsManager.runCommand(button.commandName, options);
@@ -250,11 +243,12 @@ function _handleToolbarButtonClick(button, evt, props) {
// TODO: We can update this to be a `getter` on the extension to query
// For the active tools after we apply our updates?
if (button.type === 'setToolActive') {
this.setState({
activeButtons: [button.id],
});
const toggables = activeButtons.filter(
({ options }) => options && !options.togglable
);
this.setState({ activeButtons: [...toggables, button] });
} else if (button.type === 'builtIn') {
this._handleBuiltIn(button.options);
this._handleBuiltIn(button);
}
}
@@ -279,21 +273,47 @@ function _getVisibleToolbarButtons() {
return toolbarButtonDefinitions;
}
function _handleBuiltIn({ behavior } = {}) {
if (behavior === 'CINE') {
this.setState({
isCineDialogOpen: !this.state.isCineDialogOpen,
});
function _handleBuiltIn(button) {
/* TODO: Keep cine button active until its unselected. */
const { dialog, modal, t } = this.props;
const { dialogId } = this.state;
const { id, options } = button;
if (options.behavior === 'CINE') {
if (dialogId) {
dialog.dismiss({ id: dialogId });
this.setState(state => ({
dialogId: null,
activeButtons: [
...state.activeButtons.filter(button => button.id !== id),
],
}));
} else {
const spacing = 20;
const { x, y } = document
.querySelector(`.ViewerMain`)
.getBoundingClientRect();
const newDialogId = dialog.create({
content: ConnectedCineDialog,
defaultPosition: {
x: x + spacing || 0,
y: y + spacing || 0,
},
});
this.setState(state => ({
dialogId: newDialogId,
activeButtons: [...state.activeButtons, button],
}));
}
}
if (behavior === 'DOWNLOAD_SCREEN_SHOT') {
this.props.modal.show(ConnectedViewportDownloadForm, {
title: this.props.t('Download High Quality Image'),
customClassName: 'ViewportDownloadForm',
if (options.behavior === 'DOWNLOAD_SCREEN_SHOT') {
modal.show(ConnectedViewportDownloadForm, {
title: t('Download High Quality Image'),
});
}
}
export default withTranslation(['Common', 'ViewportDownloadForm'])(
withModal(ToolbarRow)
withModal(withDialog(ToolbarRow))
);
@@ -1,191 +1,346 @@
import React, { Component } from 'react';
import React, { useState, useEffect, useContext } from 'react';
import { metadata, studies, utils, log } from '@ohif/core';
import ConnectedViewer from './ConnectedViewer.js';
import PropTypes from 'prop-types';
import { extensionManager } from './../App.js';
import { withSnackbar } from '@ohif/ui';
import { useSnackbarContext } from '@ohif/ui';
const { OHIFStudyMetadata, OHIFSeriesMetadata } = metadata;
const { retrieveStudiesMetadata, deleteStudyMetadataPromise } = studies;
const { studyMetadataManager, updateMetaDataManager } = utils;
const { studyMetadataManager, updateMetaDataManager, makeCancelable } = utils;
class ViewerRetrieveStudyData extends Component {
static propTypes = {
studyInstanceUids: PropTypes.array.isRequired,
seriesInstanceUids: PropTypes.array,
server: PropTypes.object,
// Contexts
import AppContext from '../context/AppContext';
const _promoteToFront = (list, value, searchMethod) => {
let response = [...list];
let promoted = false;
const index = response.findIndex(searchMethod.bind(undefined, value));
if (index > 0) {
const first = response.splice(index, 1);
response = [...first, ...response];
}
if (index >= 0) {
promoted = true;
}
return {
promoted,
data: response,
};
};
constructor(props) {
super(props);
this.abortSeriesLoad = false;
this.seriesLoadStats = Object.create(null);
this.state = {
studies: null,
error: null,
};
/**
* Promote series to front if find found equivalent on filters object
* @param {Object} study - study reference to promote series against
* @param {Object} [filters] - Object containing filters to be applied
* @param {string} [filter.seriesInstanceUID] - series instance uid to filter results against
* @param {boolean} isFilterStrategy - if filtering by query param strategy ON
*/
const _promoteList = (study, studyMetadata, filters, isFilterStrategy) => {
let promoted = false;
// Promote only if no filter should be applied
if (!isFilterStrategy) {
_sortStudyDisplaySet(study, studyMetadata);
promoted = _promoteStudyDisplaySet(study, studyMetadata, filters);
}
async loadStudies() {
try {
const { server, studyInstanceUids, seriesInstanceUids } = this.props;
const filters = {};
return promoted;
};
// Use the first, discard others
const seriesInstanceUID = seriesInstanceUids && seriesInstanceUids[0];
const _promoteStudyDisplaySet = (study, studyMetadata, filters) => {
let promoted = false;
const queryParamsLength = Object.keys(filters).length;
const shouldPromoteToFront = queryParamsLength > 0;
if (seriesInstanceUID) {
filters.seriesInstanceUID = seriesInstanceUID;
}
const studies = await retrieveStudiesMetadata(
server,
studyInstanceUids,
filters
);
this.validateFilters(studies, filters);
this.setStudies(studies);
} catch (e) {
this.setState({ error: true });
log.error(e);
}
}
/**
* Validate filters and promp user a message in case filter is unsuccessfully applied.
* In case of success, studies array contains, as the first element, the queried content (from filter)
* @param {Array} studies array of studies to be evaluated
* @param {Object} filters filters to test against
*/
validateFilters(studies = [], filters = {}) {
if (shouldPromoteToFront) {
const { seriesInstanceUID } = filters;
const { snackbarContext } = this.props;
// skip in case no filter or no toast manager
if (!seriesInstanceUID || !snackbarContext) {
return;
}
const _seriesLookup = (valueToCompare, displaySet) => {
return displaySet.seriesInstanceUid === valueToCompare;
};
const promotedResponse = _promoteToFront(
studyMetadata.getDisplaySets(),
seriesInstanceUID,
_seriesLookup
);
const firstStudy = studies[0] || {};
const { seriesList = [] } = firstStudy;
const firstSeries = seriesList[0];
if (!firstSeries || firstSeries.seriesInstanceUid !== seriesInstanceUID) {
snackbarContext.show({
message: 'No series for given filter: ' + seriesInstanceUID,
});
}
study.displaySets = promotedResponse.data;
promoted = promotedResponse.promoted;
}
setStudies(givenStudies) {
if (Array.isArray(givenStudies) && givenStudies.length > 0) {
const sopClassHandlerModules =
extensionManager.modules['sopClassHandlerModule'];
return promoted;
};
/**
* Method to identify if query param (from url) was applied to given list
* @param {Object} study - study reference to promote series against
* @param {Object} [filters] - Object containing filters to be applied
* @param {string} [filter.seriesInstanceUID] - series instance uid to filter results against
* @param {boolean} isFilterStrategy - if filtering by query param strategy ON
*/
const _isQueryParamApplied = (study, filters = {}, isFilterStrategy) => {
const { seriesInstanceUID } = filters;
let applied = true;
// skip in case no filter or no toast manager
if (!seriesInstanceUID) {
return applied;
}
const { seriesList = [], displaySets = [] } = study;
const firstSeries = isFilterStrategy ? seriesList[0] : displaySets[0];
if (!firstSeries || firstSeries.seriesInstanceUid !== seriesInstanceUID) {
applied = false;
}
return applied;
};
const _showUserMessage = (queryParamApplied, message, dialog = {}) => {
if (queryParamApplied) {
return;
}
const { show: showUserMessage = () => { } } = dialog;
showUserMessage({
message,
});
};
const _addSeriesToStudy = (studyMetadata, series) => {
const sopClassHandlerModules =
extensionManager.modules['sopClassHandlerModule'];
const study = studyMetadata.getData();
const seriesMetadata = new OHIFSeriesMetadata(series, study);
studyMetadata.addSeries(seriesMetadata);
studyMetadata.createAndAddDisplaySetsForSeries(
sopClassHandlerModules,
seriesMetadata,
false
);
study.displaySets = studyMetadata.getDisplaySets();
_updateMetaDataManager(study, series.seriesInstanceUid);
};
const _updateMetaDataManager = (study, studyMetadata, series) => {
updateMetaDataManager(study, series);
const { studyInstanceUID } = study;
if (!studyMetadataManager.get(studyInstanceUID)) {
studyMetadataManager.add(studyMetadata);
}
};
const _updateStudyDisplaySets = (study, studyMetadata) => {
const sopClassHandlerModules =
extensionManager.modules['sopClassHandlerModule'];
if (!study.displaySets) {
study.displaySets = studyMetadata.createDisplaySets(sopClassHandlerModules);
}
studyMetadata.setDisplaySets(study.displaySets);
};
const _sortStudyDisplaySet = (study, studyMetadata) => {
studyMetadata.sortDisplaySets(study.displaySets);
};
const _loadRemainingSeries = studyMetadata => {
const { seriesLoader } = studyMetadata.getData();
if (!seriesLoader) {
return Promise.resolve();
}
const promisesLoaders = [];
while (seriesLoader.hasNext()) {
promisesLoaders.push(
seriesLoader
.next()
.then(
series => void _addSeriesToStudy(studyMetadata, series),
error => void log.error(error)
)
);
}
return Promise.all(promisesLoaders);
};
function ViewerRetrieveStudyData({
server,
studyInstanceUids,
seriesInstanceUids,
clearViewportSpecificData,
}) {
// hooks
const [error, setError] = useState(false);
const [studies, setStudies] = useState([]);
const snackbarContext = useSnackbarContext();
const { appConfig = {} } = useContext(AppContext);
const { filterQueryParam: isFilterStrategy = false } = appConfig;
let cancelableSeriesPromises;
let cancelableStudiesPromises;
/**
* Callback method when study is totally loaded
* @param {object} study study loaded
* @param {object} studyMetadata studyMetadata for given study
* @param {Object} [filters] - Object containing filters to be applied
* @param {string} [filter.seriesInstanceUID] - series instance uid to filter results against
*/
const studyDidLoad = (study, studyMetadata, filters) => {
// User message
const promoted = _promoteList(
study,
studyMetadata,
filters,
isFilterStrategy
);
// Clear viewport to allow new promoted one to be displayed
if (promoted) {
clearViewportSpecificData(0);
}
const isQueryParamApplied = _isQueryParamApplied(
study,
filters,
isFilterStrategy
);
// Show message in case not promoted neither filtered but should to
_showUserMessage(
isQueryParamApplied,
'Query parameters were not applied. Using original series list for given study.',
snackbarContext
);
setStudies([...studies, study]);
};
/**
* Method to process studies. It will update displaySet, studyMetadata, load remaining series, ...
* @param {Array} studiesData Array of studies retrieved from server
* @param {Object} [filters] - Object containing filters to be applied
* @param {string} [filter.seriesInstanceUID] - series instance uid to filter results against
*/
const processStudies = (studiesData, filters) => {
if (Array.isArray(studiesData) && studiesData.length > 0) {
// Map studies to new format, update metadata manager?
const studies = givenStudies.map(study => {
const studies = studiesData.map(study => {
const studyMetadata = new OHIFStudyMetadata(
study,
study.studyInstanceUid
);
if (!study.displaySets) {
study.displaySets = studyMetadata.createDisplaySets(
sopClassHandlerModules
);
}
studyMetadata.setDisplaySets(study.displaySets);
// Updates WADO-RS metaDataManager
updateMetaDataManager(study);
studyMetadataManager.add(studyMetadata);
_updateStudyDisplaySets(study, studyMetadata);
_updateMetaDataManager(study, studyMetadata);
// Attempt to load remaning series if any
this._attemptToLoadRemainingSeries(studyMetadata);
cancelableSeriesPromises[study.studyInstanceUid] = makeCancelable(
_loadRemainingSeries(studyMetadata)
)
.then(result => {
if (result && !result.isCanceled) {
studyDidLoad(study, studyMetadata, filters);
}
})
.catch(error => {
if (error && !error.isCanceled) {
setError(true);
}
});
return study;
});
this.setState({ studies });
}
}
_addSeriesToStudy(studyMetadata, series) {
const sopClassHandlerModules =
extensionManager.modules['sopClassHandlerModule'];
const study = studyMetadata.getData();
const seriesMetadata = new OHIFSeriesMetadata(series, study);
studyMetadata.addSeries(seriesMetadata);
studyMetadata.createAndAddDisplaySetsForSeries(
sopClassHandlerModules,
seriesMetadata
);
study.displaySets = studyMetadata.getDisplaySets();
updateMetaDataManager(study, series.seriesInstanceUid);
this.setState(function(state) {
return { studies: state.studies.slice() };
});
}
_handleSeriesLoadResult(error, studyMetadata, series) {
if (this.abortSeriesLoad) return;
const stats = this.seriesLoadStats[studyMetadata.getStudyInstanceUID()];
if (!stats) return;
stats.count--;
if (error || !series) {
stats.errors++;
log.error(error || 'Bad Series');
return;
setStudies(studies);
}
this._addSeriesToStudy(studyMetadata, series);
}
};
_attemptToLoadRemainingSeries(studyMetadata) {
const { seriesLoader } = studyMetadata.getData();
if (!seriesLoader) {
return;
}
const stats = (this.seriesLoadStats[studyMetadata.getStudyInstanceUID()] = {
errors: 0,
count: 0,
});
while (seriesLoader.hasNext()) {
seriesLoader
.next()
.then(
series =>
void this._handleSeriesLoadResult(null, studyMetadata, series),
error => void this._handleSeriesLoadResult({ error }, null, null)
);
stats.count++;
}
}
const loadStudies = async () => {
try {
const filters = {};
// Use the first, discard others
const seriesInstanceUID = seriesInstanceUids && seriesInstanceUids[0];
componentWillUnmount() {
this.abortSeriesLoad = true;
for (const studyInstanceUid in this.seriesLoadStats) {
const stats = this.seriesLoadStats[studyInstanceUid];
if (stats && (stats.count > 0 || stats.errors > 0)) {
deleteStudyMetadataPromise(studyInstanceUid);
studyMetadataManager.remove(studyInstanceUid);
log.info(`Purging incomplete study data: ${studyInstanceUid}`);
const retrieveParams = [server, studyInstanceUids];
if (seriesInstanceUID) {
filters.seriesInstanceUID = seriesInstanceUID;
// Query param filtering controlled by appConfig property
if (isFilterStrategy) {
retrieveParams.push(filters);
}
}
cancelableStudiesPromises[studyInstanceUids] = makeCancelable(
retrieveStudiesMetadata(...retrieveParams)
)
.then(result => {
if (result && !result.isCanceled) {
processStudies(result, filters);
}
})
.catch(error => {
if (error && !error.isCanceled) {
setError(true);
}
});
} catch (error) {
if (error) {
setError(true);
}
}
}
};
componentDidMount() {
// TODO: CLEAR THIS SOMEWHERE ELSE
studyMetadataManager.purge();
this.loadStudies();
}
render() {
if (this.state.error) {
return <div>Error: {JSON.stringify(this.state.error)}</div>;
const purgeCancellablePromises = () => {
for (let studyInstanceUids in cancelableStudiesPromises) {
if ('cancel' in cancelableStudiesPromises[studyInstanceUids]) {
cancelableStudiesPromises[studyInstanceUids].cancel();
}
}
return (
<ConnectedViewer
studies={this.state.studies}
studyInstanceUids={this.props.studyInstanceUids}
/>
);
for (let studyInstanceUids in cancelableSeriesPromises) {
if ('cancel' in cancelableSeriesPromises[studyInstanceUids]) {
cancelableSeriesPromises[studyInstanceUids].cancel();
deleteStudyMetadataPromise(studyInstanceUids);
studyMetadataManager.remove(studyInstanceUids);
}
}
};
useEffect(() => {
studyMetadataManager.purge();
purgeCancellablePromises();
}, [studyInstanceUids]);
useEffect(() => {
cancelableSeriesPromises = {};
cancelableStudiesPromises = {};
loadStudies();
return () => {
purgeCancellablePromises();
};
}, []);
if (error) {
return <div>Error: {JSON.stringify(error)}</div>;
}
return (
<ConnectedViewer studies={studies} studyInstanceUids={studyInstanceUids} />
);
}
export default withSnackbar(ViewerRetrieveStudyData);
ViewerRetrieveStudyData.propTypes = {
studyInstanceUids: PropTypes.array.isRequired,
seriesInstanceUids: PropTypes.array,
server: PropTypes.object,
clearViewportSpecificData: PropTypes.func.isRequired,
};
export default ViewerRetrieveStudyData;
@@ -1,6 +1,5 @@
import React, { useState, useEffect, useContext } from 'react';
import PropTypes from 'prop-types';
import Dropzone from 'react-dropzone';
import OHIF from '@ohif/core';
import { withRouter } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
@@ -113,7 +112,8 @@ function StudyListRoute(props) {
pageNumber,
displaySize,
server,
]);
]
);
// TODO: Update Server
// if (this.props.server !== prevProps.server) {
@@ -188,10 +188,12 @@ function StudyListRoute(props) {
}
function handleFilterChange(fieldName, value) {
const updatedFilterValues = Object.assign({}, filterValues);
updatedFilterValues[fieldName] = value;
setFilterValues(updatedFilterValues);
setFilterValues(state => {
return {
...state,
[fieldName]: value,
};
});
}
return (
+8
View File
@@ -31,4 +31,12 @@
/** z-Index **/
--snackbar-zIndex: 10;
/** DatePicker Colors **/
--datePicker-input-background: #2c363f;
--datePicker-picker-background: #9ccef9;
--datePicker-picker-disabled: #d3d3d3;
--datePicker-picker-text: #484848;
--datePicker-picker-text-disabled: #aaa;
--datePicker-picker-text-input: #fff;
}
+58 -2
View File
@@ -1100,13 +1100,34 @@
pirates "^4.0.0"
source-map-support "^0.5.9"
"@babel/runtime@7.1.2", "@babel/runtime@7.5.5", "@babel/runtime@7.6.0", "@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.2.0", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.0", "@babel/runtime@^7.4.2", "@babel/runtime@^7.4.4", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.0", "@babel/runtime@^7.6.3":
"@babel/runtime@7.1.2":
version "7.1.2"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.1.2.tgz#81c89935f4647706fc54541145e6b4ecfef4b8e3"
integrity sha512-Y3SCjmhSupzFB6wcv1KmmFucH6gDVnI30WjOcicV10ju0cZjak3Jcs67YLIXBrmZYw1xCrVeJPbycFwrqNyxpg==
dependencies:
regenerator-runtime "^0.12.0"
"@babel/runtime@7.6.0":
version "7.6.0"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.6.0.tgz#4fc1d642a9fd0299754e8b5de62c631cf5568205"
integrity sha512-89eSBLJsxNxOERC0Op4vd+0Bqm6wRMqMbFtV3i0/fbaWw/mJ8Q3eBvgX0G4SyrOOLCtbu98HspF8o09MRT+KzQ==
dependencies:
regenerator-runtime "^0.13.2"
"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.2.0", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.0", "@babel/runtime@^7.4.2", "@babel/runtime@^7.4.4", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.5":
version "7.5.5"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.5.5.tgz#74fba56d35efbeca444091c7850ccd494fd2f132"
integrity sha512-28QvEGyQyNkB0/m2B4FU7IEZGK2NUrcMtT6BZEFALTguLk+AUT6ofsHtPk5QyjAdUkpMJ+/Em+quwz4HOt30AQ==
dependencies:
regenerator-runtime "^0.13.2"
"@babel/runtime@^7.6.0", "@babel/runtime@^7.6.3":
version "7.7.2"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.7.2.tgz#111a78002a5c25fc8e3361bedc9529c696b85a6a"
integrity sha512-JONRbXbTXc9WQE2mAZd1p0Z3DZ/6vaQIkgYMSTP3KjRCyd7rCZCcfhCyX+YjwcKxcZ82UrxbRD358bpExNgrjw==
dependencies:
regenerator-runtime "^0.13.2"
"@babel/template@^7.0.0", "@babel/template@^7.1.0", "@babel/template@^7.4.0", "@babel/template@^7.4.4", "@babel/template@^7.6.0":
version "7.6.0"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.6.0.tgz#7f0159c7f5012230dad64cca42ec9bdb5c9536e6"
@@ -8074,6 +8095,11 @@ executable@4.1.1:
dependencies:
pify "^2.2.0"
exenv@^1.2.0:
version "1.2.2"
resolved "https://registry.yarnpkg.com/exenv/-/exenv-1.2.2.tgz#2ae78e85d9894158670b03d47bec1f03bd91bb9d"
integrity sha1-KueOhdmJQVhnCwPUe+wfA72Ru50=
exif-parser@^0.1.12, exif-parser@^0.1.9:
version "0.1.12"
resolved "https://registry.yarnpkg.com/exif-parser/-/exif-parser-0.1.12.tgz#58a9d2d72c02c1f6f02a0ef4a9166272b7760922"
@@ -16019,6 +16045,14 @@ react-dom@^16.8.6:
prop-types "^15.6.2"
scheduler "^0.17.0"
react-draggable@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/react-draggable/-/react-draggable-4.1.0.tgz#e1c5b774001e32f0bff397254e1e9d5448ac92a4"
integrity sha512-Or/qe70cfymshqoC8Lsp0ukTzijJObehb7Vfl7tb5JRxoV+b6PDkOGoqYaWBzZ59k9dH/bwraLGsnlW78/3vrA==
dependencies:
classnames "^2.2.5"
prop-types "^15.6.0"
react-dropzone@^10.1.7:
version "10.1.10"
resolved "https://registry.yarnpkg.com/react-dropzone/-/react-dropzone-10.1.10.tgz#f340290dfc26ac09ad68abc020ab6232c23d6cf3"
@@ -16082,7 +16116,7 @@ react-is@^16.3.2, react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1, react-is
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.11.0.tgz#b85dfecd48ad1ce469ff558a882ca8e8313928fa"
integrity sha512-gbBVYR2p8mnriqAwWx9LbuUrShnAuSCNnuPGyc7GJrMVQtPDAh8iLpv7FRuMPFb56KkaVZIYSz1PrjI9q0QPCw==
react-lifecycles-compat@^3.0.4:
react-lifecycles-compat@^3.0.0, react-lifecycles-compat@^3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"
integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==
@@ -16101,6 +16135,16 @@ react-live@2.0.1:
react-simple-code-editor "^0.9.0"
unescape "^0.2.0"
react-modal@^3.11.1:
version "3.11.1"
resolved "https://registry.yarnpkg.com/react-modal/-/react-modal-3.11.1.tgz#2a0d6877c9e98f123939ea92d2bb4ad7fa5a17f9"
integrity sha512-8uN744Yq0X2lbfSLxsEEc2UV3RjSRb4yDVxRQ1aGzPo86QjNOwhQSukDb8U8kR+636TRTvfMren10fgOjAy9eA==
dependencies:
exenv "^1.2.0"
prop-types "^15.5.10"
react-lifecycles-compat "^3.0.0"
warning "^4.0.3"
react-moment-proptypes@^1.6.0:
version "1.7.0"
resolved "https://registry.yarnpkg.com/react-moment-proptypes/-/react-moment-proptypes-1.7.0.tgz#89881479840a76c13574a86e3bb214c4ba564e7a"
@@ -16613,6 +16657,11 @@ regenerator-runtime@^0.11.0, regenerator-runtime@^0.11.1:
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9"
integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==
regenerator-runtime@^0.12.0:
version "0.12.1"
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.12.1.tgz#fa1a71544764c036f8c49b13a08b2594c9f8a0de"
integrity sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg==
regenerator-runtime@^0.13.1, regenerator-runtime@^0.13.2:
version "0.13.3"
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.3.tgz#7cf6a77d8f5c6f60eb73c5fc1955b2ceb01e6bf5"
@@ -20041,6 +20090,13 @@ warning@^3.0.0:
dependencies:
loose-envify "^1.0.0"
warning@^4.0.3:
version "4.0.3"
resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.3.tgz#16e9e077eb8a86d6af7d64aa1e05fd85b4678ca3"
integrity sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==
dependencies:
loose-envify "^1.0.0"
watchpack@^1.6.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.6.0.tgz#4bc12c2ebe8aa277a71f1d3f14d685c7b446cd00"