Compare commits
39
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0bb3eeb068 | ||
|
|
1df21a9e07 | ||
|
|
fd1a9e2abe | ||
|
|
e34a057263 | ||
|
|
27e02825a4 | ||
|
|
8fd3af1e13 | ||
|
|
1334ba8eb0 | ||
|
|
a870b3cc60 | ||
|
|
52687a7113 | ||
|
|
421de23ba3 | ||
|
|
adc8d9ea0f | ||
|
|
f65639c2b0 | ||
|
|
cdd75bda2a | ||
|
|
d92fe44d3c | ||
|
|
d90c2ac024 | ||
|
|
7ce5e1d2d3 | ||
|
|
a3bf47f9bf | ||
|
|
81f79a26ad | ||
|
|
60b4b5d7cf | ||
|
|
69c54d4f49 | ||
|
|
0f8868a232 | ||
|
|
1bbfee4087 | ||
|
|
33c7b59611 | ||
|
|
b17f753e62 | ||
|
|
c77a2ef5c9 | ||
|
|
348edd4717 | ||
|
|
407060658a | ||
|
|
fc00f9d505 | ||
|
|
bc3d080672 | ||
|
|
09353354c3 | ||
|
|
0a12320083 | ||
|
|
813ee5ed4d | ||
|
|
6085b5f733 | ||
|
|
ffe12789dd | ||
|
|
0b68429dfb | ||
|
|
b747ea5144 | ||
|
|
caadf089df | ||
|
|
e0fc69c181 | ||
|
|
2b9765304f |
No files matched your search
@@ -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
|
||||
|
||||
###
|
||||
|
||||
@@ -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: ''
|
||||
---
|
||||
|
||||
|
||||
@@ -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
@@ -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:'
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
# Specific to our non-deploy-preview deploys
|
||||
# Confgure redirects using netlify.toml
|
||||
|
||||
# PWA Redirect
|
||||
/* /index.html 200
|
||||
@@ -1,5 +1,6 @@
|
||||
# OHIF Viewers
|
||||
|
||||
- [Our Process](our-process.md)
|
||||
- Essentials
|
||||
- [Getting Started](essentials/getting-started.md)
|
||||
- [Installation](essentials/installation.md)
|
||||
|
||||
@@ -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 -->
|
||||
@@ -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:
|
||||
|
||||
|
||||
@@ -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 -->
|
||||
@@ -3,6 +3,28 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [1.7.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@1.6.0...@ohif/extension-cornerstone@1.7.0) (2019-11-25)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add new annotate tool using new dialog service ([#1211](https://github.com/OHIF/Viewers/issues/1211)) ([8fd3af1](https://github.com/OHIF/Viewers/commit/8fd3af1e137e793f1b482760a22591c64a072047))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [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,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/extension-cornerstone",
|
||||
"version": "1.5.1",
|
||||
"version": "1.7.0",
|
||||
"description": "OHIF extension for Cornerstone",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
@@ -32,7 +32,7 @@
|
||||
"@ohif/ui": "^0.50.0",
|
||||
"cornerstone-core": "^2.2.8",
|
||||
"cornerstone-math": "^0.1.8",
|
||||
"cornerstone-tools": "^4.0.9",
|
||||
"cornerstone-tools": "^4.8.0",
|
||||
"cornerstone-wado-image-loader": "^3.0.0",
|
||||
"dcmjs": "^0.6.1",
|
||||
"dicom-parser": "^1.8.3",
|
||||
|
||||
@@ -3,6 +3,7 @@ import cornerstone from 'cornerstone-core';
|
||||
import csTools from 'cornerstone-tools';
|
||||
import initCornerstoneTools from './initCornerstoneTools.js';
|
||||
import queryString from 'query-string';
|
||||
import { SimpleDialog } from '@ohif/ui';
|
||||
|
||||
function fallbackMetaDataProvider(type, imageId) {
|
||||
if (!imageId.includes('wado?requestType=WADO')) {
|
||||
@@ -29,6 +30,28 @@ cornerstone.metaData.addProvider(fallbackMetaDataProvider, -1);
|
||||
* @param {Object|Array} configuration.csToolsConfig
|
||||
*/
|
||||
export default function init({ servicesManager, configuration = {} }) {
|
||||
const { UIDialogService } = servicesManager.services;
|
||||
const callInputDialog = (data, event, callback) => {
|
||||
let dialogId = UIDialogService.create({
|
||||
content: SimpleDialog.InputDialog,
|
||||
defaultPosition: {
|
||||
x: (event && event.currentPoints.canvas.x) || 0,
|
||||
y: (event && event.currentPoints.canvas.y) || 0,
|
||||
},
|
||||
showOverlay: true,
|
||||
contentProps: {
|
||||
title: 'Enter your annotation',
|
||||
label: 'New label',
|
||||
measurementData: data ? { description: data.text } : {},
|
||||
onClose: () => UIDialogService.dismiss({ id: dialogId }),
|
||||
onSubmit: value => {
|
||||
callback(value);
|
||||
UIDialogService.dismiss({ id: dialogId });
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const { csToolsConfig } = configuration;
|
||||
const { StackManager } = OHIF.utils;
|
||||
const metadataProvider = new OHIF.cornerstone.MetadataProvider();
|
||||
@@ -62,7 +85,6 @@ export default function init({ servicesManager, configuration = {} }) {
|
||||
ZoomTouchPinchTool,
|
||||
// Annotations
|
||||
EraserTool,
|
||||
ArrowAnnotateTool,
|
||||
BidirectionalTool,
|
||||
LengthTool,
|
||||
AngleTool,
|
||||
@@ -85,7 +107,6 @@ export default function init({ servicesManager, configuration = {} }) {
|
||||
ZoomTouchPinchTool,
|
||||
// Annotations
|
||||
EraserTool,
|
||||
ArrowAnnotateTool,
|
||||
BidirectionalTool,
|
||||
LengthTool,
|
||||
AngleTool,
|
||||
@@ -99,6 +120,15 @@ export default function init({ servicesManager, configuration = {} }) {
|
||||
|
||||
tools.forEach(tool => csTools.addTool(tool));
|
||||
|
||||
csTools.addTool(csTools.ArrowAnnotateTool, {
|
||||
configuration: {
|
||||
getTextCallback: (callback, eventDetails) =>
|
||||
callInputDialog(null, eventDetails, callback),
|
||||
changeTextCallback: (data, eventDetails, callback) =>
|
||||
callInputDialog(data, eventDetails, callback),
|
||||
},
|
||||
});
|
||||
|
||||
csTools.setToolActive('Pan', { mouseButtonMask: 4 });
|
||||
csTools.setToolActive('Zoom', { mouseButtonMask: 2 });
|
||||
csTools.setToolActive('Wwwc', { mouseButtonMask: 1 });
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -3,6 +3,65 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [0.54.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.54.0...@ohif/extension-vtk@0.54.1) (2019-11-28)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [0.54.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.53.13...@ohif/extension-vtk@0.54.0) (2019-11-25)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add new annotate tool using new dialog service ([#1211](https://github.com/OHIF/Viewers/issues/1211)) ([8fd3af1](https://github.com/OHIF/Viewers/commit/8fd3af1e137e793f1b482760a22591c64a072047))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.53.13](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.53.12...@ohif/extension-vtk@0.53.13) (2019-11-25)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.53.12](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.53.11...@ohif/extension-vtk@0.53.12) (2019-11-20)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.53.11](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.53.10...@ohif/extension-vtk@0.53.11) (2019-11-19)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.53.10](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.53.9...@ohif/extension-vtk@0.53.10) (2019-11-19)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.53.9](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.53.8...@ohif/extension-vtk@0.53.9) (2019-11-18)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.53.8](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.53.7...@ohif/extension-vtk@0.53.8) (2019-11-15)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/extension-vtk",
|
||||
"version": "0.53.8",
|
||||
"version": "0.54.1",
|
||||
"description": "OHIF extension for VTK.js",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
@@ -52,9 +52,9 @@
|
||||
"react-vtkjs-viewport": "^0.3.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ohif/core": "^1.10.0",
|
||||
"@ohif/ui": "^0.62.3",
|
||||
"cornerstone-tools": "^4.0.9",
|
||||
"@ohif/core": "^1.13.1",
|
||||
"@ohif/ui": "^0.65.1",
|
||||
"cornerstone-tools": "^4.8.0",
|
||||
"cornerstone-wado-image-loader": "^3.0.0",
|
||||
"dcmjs": "^0.6.1",
|
||||
"dicom-parser": "^1.8.3",
|
||||
|
||||
@@ -3,6 +3,50 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.13.1](https://github.com/OHIF/Viewers/compare/@ohif/core@1.13.0...@ohif/core@1.13.1) (2019-11-28)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* User Preferences Issues ([#1207](https://github.com/OHIF/Viewers/issues/1207)) ([1df21a9](https://github.com/OHIF/Viewers/commit/1df21a9e075b5e6dfc10a429ae825826f46c71b8)), closes [#1161](https://github.com/OHIF/Viewers/issues/1161) [#1164](https://github.com/OHIF/Viewers/issues/1164) [#1177](https://github.com/OHIF/Viewers/issues/1177) [#1179](https://github.com/OHIF/Viewers/issues/1179) [#1180](https://github.com/OHIF/Viewers/issues/1180) [#1181](https://github.com/OHIF/Viewers/issues/1181) [#1182](https://github.com/OHIF/Viewers/issues/1182) [#1183](https://github.com/OHIF/Viewers/issues/1183) [#1184](https://github.com/OHIF/Viewers/issues/1184) [#1185](https://github.com/OHIF/Viewers/issues/1185)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [1.13.0](https://github.com/OHIF/Viewers/compare/@ohif/core@1.12.0...@ohif/core@1.13.0) (2019-11-25)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add new annotate tool using new dialog service ([#1211](https://github.com/OHIF/Viewers/issues/1211)) ([8fd3af1](https://github.com/OHIF/Viewers/commit/8fd3af1e137e793f1b482760a22591c64a072047))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [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)
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/core",
|
||||
"version": "1.10.0",
|
||||
"version": "1.13.1",
|
||||
"description": "Generic business logic for web-based medical imaging applications",
|
||||
"author": "OHIF Core Team",
|
||||
"license": "MIT",
|
||||
@@ -32,7 +32,7 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"cornerstone-core": "^2.2.8",
|
||||
"cornerstone-tools": "^4.0.9",
|
||||
"cornerstone-tools": "^4.8.0",
|
||||
"cornerstone-wado-image-loader": "^3.0.0",
|
||||
"dicom-parser": "^1.8.3"
|
||||
},
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import cloneDeep from 'lodash.clonedeep';
|
||||
import hotkeys from './hotkeys';
|
||||
import log from './../log.js';
|
||||
|
||||
@@ -44,20 +43,68 @@ export class HotkeysManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a list of hotkeydefinitions. Optionally, sets the
|
||||
* default hotkey bindings for all provided definitions. These
|
||||
* Registers a list of hotkeydefinitions.
|
||||
*
|
||||
* @param {HotkeyDefinition[] | Object} hotkeyDefinitions Contains hotkeys definitions
|
||||
*/
|
||||
setHotkeys(hotkeyDefinitions) {
|
||||
const definitions = Array.isArray(hotkeyDefinitions)
|
||||
? [...hotkeyDefinitions]
|
||||
: this._parseToArrayLike(hotkeyDefinitions);
|
||||
|
||||
definitions.forEach(definition => this.registerHotkeys(definition));
|
||||
}
|
||||
|
||||
/**
|
||||
* Set default hotkey bindings. These
|
||||
* values are used in `this.restoreDefaultBindings`.
|
||||
*
|
||||
* @param {HotkeyDefinition[]} hotkeyDefinitions
|
||||
* @param {Boolean} [isDefaultDefinitions]
|
||||
* @param {HotkeyDefinition[] | Object} hotkeyDefinitions Contains hotkeys definitions
|
||||
*/
|
||||
setHotkeys(hotkeyDefinitions, isDefaultDefinitions = false) {
|
||||
const definitions = cloneDeep(hotkeyDefinitions);
|
||||
definitions.forEach(definition => this.registerHotkeys(definition));
|
||||
setDefaultHotKeys(hotkeyDefinitions) {
|
||||
const definitions = Array.isArray(hotkeyDefinitions)
|
||||
? [...hotkeyDefinitions]
|
||||
: this._parseToArrayLike(hotkeyDefinitions);
|
||||
|
||||
if (isDefaultDefinitions) {
|
||||
this.hotkeyDefaults = definitions;
|
||||
}
|
||||
this.hotkeyDefaults = definitions;
|
||||
}
|
||||
|
||||
/**
|
||||
* It parses given object containing hotkeyDefinition to array like.
|
||||
* Each property of given object will be mapped to an object of an array. And its property name will be the value of a property named as commandName
|
||||
*
|
||||
* @param {HotkeyDefinition[] | Object} hotkeyDefinitions Contains hotkeys definitions
|
||||
* @returns {HotkeyDefinition[]}
|
||||
*/
|
||||
_parseToArrayLike(hotkeyDefinitionsObj) {
|
||||
const copy = { ...hotkeyDefinitionsObj };
|
||||
return Object.entries(copy).map(entryValue =>
|
||||
this._parseToHotKeyObj(entryValue[0], entryValue[1])
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return HotkeyDefinition object like based on given property name and property value
|
||||
* @param {string} propertyName property name of hotkey definition object
|
||||
* @param {object} propertyValue property value of hotkey definition object
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* const hotKeyObj = {hotKeyDefA: {keys:[],....}}
|
||||
*
|
||||
* const parsed = _parseToHotKeyObj(Object.keys(hotKeyDefA)[0], hotKeyObj[hotKeyDefA]);
|
||||
* {
|
||||
* commandName: hotKeyDefA,
|
||||
* keys: [],
|
||||
* ....
|
||||
* }
|
||||
*
|
||||
*/
|
||||
_parseToHotKeyObj(propertyName, propertyValue) {
|
||||
return {
|
||||
commandName: propertyName,
|
||||
...propertyValue,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -94,18 +94,20 @@ describe('HotkeysManager', () => {
|
||||
expect(firstCallArgs).toEqual(hotkeyDefinitions[0]);
|
||||
expect(secondCallArgs).toEqual(hotkeyDefinitions[1]);
|
||||
});
|
||||
it('does not set this.hotkeyDefaults by default', () => {
|
||||
it('does not set this.hotkeyDefaults when calling setHotKeys', () => {
|
||||
const hotkeyDefinitions = [{ commandName: 'dance', keys: '+' }];
|
||||
|
||||
hotkeysManager.setHotkeys(hotkeyDefinitions);
|
||||
|
||||
expect(hotkeysManager.hotkeyDefaults).toEqual([]);
|
||||
});
|
||||
it('sets this.hotkeyDefaults when isDefaultDefinitions is true', () => {
|
||||
const hotkeyDefinitions = [{ commandName: 'dance', keys: '+' }];
|
||||
const isDefaultDefinitions = true;
|
||||
});
|
||||
|
||||
hotkeysManager.setHotkeys(hotkeyDefinitions, isDefaultDefinitions);
|
||||
describe('setDefaultHotKeys()', () => {
|
||||
it('it sets default hotkeys', () => {
|
||||
const hotkeyDefinitions = [{ commandName: 'dance', keys: '+' }];
|
||||
|
||||
hotkeysManager.setDefaultHotKeys(hotkeyDefinitions);
|
||||
|
||||
expect(hotkeysManager.hotkeyDefaults).toEqual(hotkeyDefinitions);
|
||||
});
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -12,6 +12,7 @@ describe('Top level exports', () => {
|
||||
//
|
||||
'createUINotificationService',
|
||||
'createUIModalService',
|
||||
'createUIDialogService',
|
||||
//
|
||||
'utils',
|
||||
'studies',
|
||||
|
||||
@@ -11,12 +11,15 @@ const defaultState = {
|
||||
// order, description, window (int), level (int)
|
||||
// 0: { description: 'Soft tissue', window: '', level: '' },
|
||||
},
|
||||
generalPreferences: {
|
||||
// language: 'en-US'
|
||||
},
|
||||
};
|
||||
|
||||
const preferences = (state, action) => {
|
||||
switch (action.type) {
|
||||
case 'SET_USER_PREFERENCES': {
|
||||
const newState = action.state ? action.state : cloneDeep(defaultState);
|
||||
const newState = action.state || cloneDeep(defaultState);
|
||||
|
||||
return Object.assign({}, state, newState);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
/**
|
||||
* A UI Element Position
|
||||
*
|
||||
* @typedef {Object} ElementPosition
|
||||
* @property {number} top -
|
||||
* @property {number} left -
|
||||
* @property {number} right -
|
||||
* @property {number} bottom -
|
||||
*/
|
||||
|
||||
/**
|
||||
* UI Dialog
|
||||
*
|
||||
* @typedef {Object} DialogProps
|
||||
* @property {string} id The dialog id.
|
||||
* @property {ReactElement|HTMLElement} content The dialog content.
|
||||
* @property {Object} contentProps The dialog content props.
|
||||
* @property {boolean} [isDraggable=true] Controls if dialog content is draggable or not.
|
||||
* @property {boolean} [showOverlay=false] Controls dialog overlay.
|
||||
* @property {ElementPosition} defaultPosition Specifies the `x` and `y` that the dragged item should start at.
|
||||
* @property {ElementPosition} position If this property is present, the item becomes 'controlled' and is not responsive to user input.
|
||||
* @property {Function} onStart Called when dragging starts. If `false` is returned any handler, the action will cancel.
|
||||
* @property {Function} onStop Called when dragging stops.
|
||||
* @property {Function} onDrag Called while dragging.
|
||||
*/
|
||||
|
||||
const uiDialogServicePublicAPI = {
|
||||
name: 'UIDialogService',
|
||||
dismiss,
|
||||
dismissAll,
|
||||
create,
|
||||
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, contentProps, onStart, onDrag, onStop, isDraggable, showOverlay, defaultPosition, position }
|
||||
*/
|
||||
function create({
|
||||
id,
|
||||
content,
|
||||
contentProps,
|
||||
onStart,
|
||||
onDrag,
|
||||
onStop,
|
||||
isDraggable = true,
|
||||
showOverlay = false,
|
||||
defaultPosition,
|
||||
position,
|
||||
}) {
|
||||
return uiDialogServiceImplementation._create({
|
||||
id,
|
||||
content,
|
||||
contentProps,
|
||||
onStart,
|
||||
onDrag,
|
||||
onStop,
|
||||
isDraggable,
|
||||
showOverlay,
|
||||
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;
|
||||
@@ -1,21 +1,14 @@
|
||||
/**
|
||||
* A UI Element
|
||||
*
|
||||
* @typedef {ReactElement|HTMLElement} Modal
|
||||
*/
|
||||
|
||||
/**
|
||||
* UI Modal
|
||||
*
|
||||
* @typedef {Object} ModalProps
|
||||
* @property {string} [header=null] -
|
||||
* @property {string} [footer=null] -
|
||||
* @property {string} [backdrop=false] -
|
||||
* @property {string} [keyboard=false] -
|
||||
* @property {number} [show=true] -
|
||||
* @property {string} [closeButton=true] -
|
||||
* @property {string} [title=null] - 'Modal Title'
|
||||
* @property {boolean} [customClassName=null] - '.ModalClass'
|
||||
* @property {ReactElement|HTMLElement} [content=null] Modal content.
|
||||
* @property {Object} [contentProps=null] Modal content props.
|
||||
* @property {boolean} [shouldCloseOnEsc=false] Modal is dismissible via the esc key.
|
||||
* @property {boolean} [isOpen=true] Make the Modal visible or hidden.
|
||||
* @property {boolean} [closeButton=true] Should the modal body render the close button.
|
||||
* @property {string} [title=null] Should the modal render the title independently of the body content.
|
||||
* @property {string} [customClassName=null] The custom class to style the modal.
|
||||
*/
|
||||
|
||||
const uiModalServicePublicAPI = {
|
||||
@@ -37,23 +30,26 @@ 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 { content, contentProps, shouldCloseOnEsc, isOpen, closeButton, title, customClassName }
|
||||
*/
|
||||
function show(
|
||||
component,
|
||||
props = {
|
||||
header: null,
|
||||
footer: null,
|
||||
backdrop: false,
|
||||
keyboard: false,
|
||||
show: true,
|
||||
closeButton: true,
|
||||
title: null,
|
||||
customClassName: null,
|
||||
}
|
||||
) {
|
||||
return uiModalServiceImplementation._show(component, props);
|
||||
function show({
|
||||
content = null,
|
||||
contentProps = null,
|
||||
shouldCloseOnEsc = false,
|
||||
isOpen = true,
|
||||
closeButton = true,
|
||||
title = null,
|
||||
customClassName = null,
|
||||
}) {
|
||||
return uiModalServiceImplementation._show({
|
||||
content,
|
||||
contentProps,
|
||||
shouldCloseOnEsc,
|
||||
isOpen,
|
||||
closeButton,
|
||||
title,
|
||||
customClassName,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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) { }
|
||||
}
|
||||
@@ -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 };
|
||||
@@ -3,6 +3,28 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [0.52.2](https://github.com/OHIF/Viewers/compare/@ohif/i18n@0.52.1...@ohif/i18n@0.52.2) (2019-11-28)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* User Preferences Issues ([#1207](https://github.com/OHIF/Viewers/issues/1207)) ([1df21a9](https://github.com/OHIF/Viewers/commit/1df21a9e075b5e6dfc10a429ae825826f46c71b8)), closes [#1161](https://github.com/OHIF/Viewers/issues/1161) [#1164](https://github.com/OHIF/Viewers/issues/1164) [#1177](https://github.com/OHIF/Viewers/issues/1177) [#1179](https://github.com/OHIF/Viewers/issues/1179) [#1180](https://github.com/OHIF/Viewers/issues/1180) [#1181](https://github.com/OHIF/Viewers/issues/1181) [#1182](https://github.com/OHIF/Viewers/issues/1182) [#1183](https://github.com/OHIF/Viewers/issues/1183) [#1184](https://github.com/OHIF/Viewers/issues/1184) [#1185](https://github.com/OHIF/Viewers/issues/1185)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.52.1](https://github.com/OHIF/Viewers/compare/@ohif/i18n@0.52.0...@ohif/i18n@0.52.1) (2019-11-18)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* minor date picker UX improvements ([813ee5e](https://github.com/OHIF/Viewers/commit/813ee5ed4d78b7bda234922d5f3389efe346451c))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [0.52.0](https://github.com/OHIF/Viewers/compare/@ohif/i18n@0.51.0...@ohif/i18n@0.52.0) (2019-11-06)
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/i18n",
|
||||
"version": "0.52.0",
|
||||
"version": "0.52.2",
|
||||
"description": "Internationalization library for The OHIF Viewer",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -42,6 +42,7 @@ const locizeOptions = {
|
||||
|
||||
const envUseLocize = !!config.USE_LOCIZE;
|
||||
const envApiKeyAvailable = !!config.LOCIZE_API_KEY;
|
||||
const DEFAULT_LANGUAGE = 'en-US';
|
||||
|
||||
function initI18n(
|
||||
detection = detectionOptions,
|
||||
@@ -74,7 +75,7 @@ function initI18n(
|
||||
// init i18next
|
||||
// for all options read: https://www.i18next.com/overview/configuration-options
|
||||
.init({
|
||||
fallbackLng: 'en-US',
|
||||
fallbackLng: DEFAULT_LANGUAGE,
|
||||
saveMissing: apiKeyAvailable,
|
||||
debug: debugMode,
|
||||
keySeparator: false,
|
||||
@@ -111,7 +112,7 @@ function initI18n(
|
||||
// init i18next
|
||||
// for all options read: https://www.i18next.com/overview/configuration-options
|
||||
.init({
|
||||
fallbackLng: 'en-US',
|
||||
fallbackLng: DEFAULT_LANGUAGE,
|
||||
resources: locales,
|
||||
debug: debugMode,
|
||||
keySeparator: false,
|
||||
@@ -136,5 +137,6 @@ customDebug(`version ${pkg.version} loaded.`, 'info');
|
||||
i18n.initializing = initI18n();
|
||||
i18n.initI18n = initI18n;
|
||||
i18n.addLocales = addLocales;
|
||||
i18n.defaultLanguage = DEFAULT_LANGUAGE;
|
||||
|
||||
export default i18n;
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"No hotkeys found": "Nenhuma tecla de atalho está configurada para este aplicativo. As teclas de atalho podem ser configuradas no arquivo app-config.js do aplicativo."
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import UserPreferencesModal from "./UserPreferencesModal.json";
|
||||
|
||||
export default {
|
||||
'ar': {
|
||||
UserPreferencesModal,
|
||||
}
|
||||
};
|
||||
@@ -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"
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
{
|
||||
"Cancel": "$t(Buttons:Cancel)",
|
||||
"No hotkeys found": "No hotkeys are configured for this application. Hotkeys can be configured in the application's app-config.js file.",
|
||||
"Reset to Defaults": "$t(Buttons:Reset to Defaults)",
|
||||
"ResetDefaultMessage": "Preferences successfully reset to default. <br /> You must <strong>Save</strong> to perform this action.",
|
||||
"Save": "$t(Buttons:Save)",
|
||||
"SaveMessage": "Preferences saved",
|
||||
"User Preferences": "User Preferences"
|
||||
}
|
||||
@@ -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,3 +1,4 @@
|
||||
import ar from './ar/';
|
||||
import en_US from './en-US/';
|
||||
import es from './es/';
|
||||
import ja_JP from './ja-JP/';
|
||||
@@ -7,6 +8,7 @@ import vi from './vi/';
|
||||
import zh from './zh/';
|
||||
|
||||
export default {
|
||||
...ar,
|
||||
...en_US,
|
||||
...es,
|
||||
...ja_JP,
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
{
|
||||
"Cancel": "Cancelar",
|
||||
"Reset to Defaults": "Restaurar Default",
|
||||
"ResetDefaultMessage": "Preferências resetadas com sucesso. <br /> Você deve <strong>Salvar</strong> para que essa ação seja realizada.",
|
||||
"Save": "Salvar",
|
||||
"SaveMessage": "Preferências salvas",
|
||||
"User Preferences": "Preferências do Usuário"
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import AboutModal from './AboutModal.json';
|
||||
import Buttons from './Buttons.json';
|
||||
import CineDialog from './CineDialog.json';
|
||||
import Common from './Common.json';
|
||||
import DatePicker from './DatePicker.json';
|
||||
import Header from './Header.json';
|
||||
import UserPreferencesModal from './UserPreferencesModal.json';
|
||||
|
||||
@@ -11,6 +12,7 @@ export default {
|
||||
Buttons,
|
||||
CineDialog,
|
||||
Common,
|
||||
DatePicker,
|
||||
Header,
|
||||
UserPreferencesModal,
|
||||
},
|
||||
|
||||
@@ -3,6 +3,80 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [0.65.1](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.65.0...@ohif/ui@0.65.1) (2019-11-28)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* User Preferences Issues ([#1207](https://github.com/OHIF/Viewers/issues/1207)) ([1df21a9](https://github.com/OHIF/Viewers/commit/1df21a9e075b5e6dfc10a429ae825826f46c71b8)), closes [#1161](https://github.com/OHIF/Viewers/issues/1161) [#1164](https://github.com/OHIF/Viewers/issues/1164) [#1177](https://github.com/OHIF/Viewers/issues/1177) [#1179](https://github.com/OHIF/Viewers/issues/1179) [#1180](https://github.com/OHIF/Viewers/issues/1180) [#1181](https://github.com/OHIF/Viewers/issues/1181) [#1182](https://github.com/OHIF/Viewers/issues/1182) [#1183](https://github.com/OHIF/Viewers/issues/1183) [#1184](https://github.com/OHIF/Viewers/issues/1184) [#1185](https://github.com/OHIF/Viewers/issues/1185)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [0.65.0](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.64.2...@ohif/ui@0.65.0) (2019-11-25)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add new annotate tool using new dialog service ([#1211](https://github.com/OHIF/Viewers/issues/1211)) ([8fd3af1](https://github.com/OHIF/Viewers/commit/8fd3af1e137e793f1b482760a22591c64a072047))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.64.2](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.64.1...@ohif/ui@0.64.2) (2019-11-25)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Issue branch from danny experimental changes pr 1128 ([#1150](https://github.com/OHIF/Viewers/issues/1150)) ([a870b3c](https://github.com/OHIF/Viewers/commit/a870b3cc6056cf824af422e46f1ad674910b534e)), closes [#1161](https://github.com/OHIF/Viewers/issues/1161) [#1164](https://github.com/OHIF/Viewers/issues/1164) [#1177](https://github.com/OHIF/Viewers/issues/1177) [#1179](https://github.com/OHIF/Viewers/issues/1179) [#1180](https://github.com/OHIF/Viewers/issues/1180) [#1181](https://github.com/OHIF/Viewers/issues/1181) [#1182](https://github.com/OHIF/Viewers/issues/1182) [#1183](https://github.com/OHIF/Viewers/issues/1183) [#1184](https://github.com/OHIF/Viewers/issues/1184) [#1185](https://github.com/OHIF/Viewers/issues/1185)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.64.1](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.64.0...@ohif/ui@0.64.1) (2019-11-20)
|
||||
|
||||
**Note:** Version bump only for package @ohif/ui
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [0.64.0](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.63.0...@ohif/ui@0.64.0) (2019-11-19)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* New dialog service ([#1202](https://github.com/OHIF/Viewers/issues/1202)) ([f65639c](https://github.com/OHIF/Viewers/commit/f65639c2b0dab01decd20cab2cef4263cb4fab37))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [0.63.0](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.62.4...@ohif/ui@0.63.0) (2019-11-19)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Issue 879 viewer route query param not filtering but promoting ([#1141](https://github.com/OHIF/Viewers/issues/1141)) ([b17f753](https://github.com/OHIF/Viewers/commit/b17f753e6222045252ef885e40233681541a32e1)), closes [#1118](https://github.com/OHIF/Viewers/issues/1118)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.62.4](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.62.3...@ohif/ui@0.62.4) (2019-11-18)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* minor date picker UX improvements ([813ee5e](https://github.com/OHIF/Viewers/commit/813ee5ed4d78b7bda234922d5f3389efe346451c))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.62.3](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.62.2...@ohif/ui@0.62.3) (2019-11-15)
|
||||
|
||||
**Note:** Version bump only for package @ohif/ui
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/ui",
|
||||
"version": "0.62.3",
|
||||
"version": "0.65.1",
|
||||
"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;
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import i18n from '@ohif/i18n';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import './LanguageSwitcher.styl';
|
||||
import { withTranslation } from '../../utils/LanguageProvider';
|
||||
import { withTranslation } from '../../contextProviders';
|
||||
|
||||
const LanguageSwitcher = () => {
|
||||
const getCurrentLanguage = (language = i18n.language) =>
|
||||
language.split('-')[0];
|
||||
const LanguageSwitcher = ({ language, onLanguageChange }) => {
|
||||
const parseLanguage = lang => lang.split('-')[0];
|
||||
|
||||
const [currentLanguage, setCurrentLanguage] = useState(getCurrentLanguage());
|
||||
const languages = [
|
||||
// TODO: list of available languages should come from i18n.options.resources
|
||||
{
|
||||
@@ -21,46 +19,31 @@ const LanguageSwitcher = () => {
|
||||
},
|
||||
];
|
||||
|
||||
const onChange = () => {
|
||||
const onChange = event => {
|
||||
const { value } = event.target;
|
||||
const language = getCurrentLanguage(value);
|
||||
setCurrentLanguage(language);
|
||||
|
||||
i18n.init({
|
||||
fallbackLng: language,
|
||||
lng: language,
|
||||
});
|
||||
onLanguageChange(parseLanguage(value));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
let mounted = true;
|
||||
|
||||
i18n.on('languageChanged', () => {
|
||||
if (mounted) {
|
||||
setCurrentLanguage(getCurrentLanguage());
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
mounted = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<select
|
||||
name="language-select"
|
||||
id="language-select"
|
||||
className="language-select"
|
||||
value={currentLanguage}
|
||||
value={parseLanguage(language)}
|
||||
onChange={onChange}
|
||||
>
|
||||
{languages.map(language => (
|
||||
<option key={language.value} value={language.value}>
|
||||
{language.label}
|
||||
{languages.map(lng => (
|
||||
<option key={lng.value} value={lng.value}>
|
||||
{lng.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
);
|
||||
};
|
||||
|
||||
LanguageSwitcher.propTypes = {
|
||||
language: PropTypes.string.isRequired,
|
||||
onLanguageChange: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default withTranslation('UserPreferencesModal')(LanguageSwitcher);
|
||||
@@ -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,5 +1,6 @@
|
||||
import React, { Component } from 'react';
|
||||
import React, { Component, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { TextInput } from '@ohif/ui';
|
||||
|
||||
import './SimpleDialog.styl';
|
||||
|
||||
@@ -21,6 +22,31 @@ class SimpleDialog extends Component {
|
||||
rootClass: '',
|
||||
};
|
||||
|
||||
static InputDialog = ({ onSubmit, defaultValue, title, label, onClose }) => {
|
||||
const [value, setValue] = useState(defaultValue);
|
||||
|
||||
const onSubmitHandler = () => {
|
||||
onSubmit(value);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="InputDialog">
|
||||
<SimpleDialog
|
||||
headerTitle={title}
|
||||
onClose={onClose}
|
||||
onConfirm={onSubmitHandler}
|
||||
>
|
||||
<TextInput
|
||||
type="text"
|
||||
value={value}
|
||||
onChange={event => setValue(event.target.value)}
|
||||
label={label}
|
||||
/>
|
||||
</SimpleDialog>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<React.Fragment>
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
@import './../../design/styles/common/button.styl'
|
||||
@import './../../design/styles/common/global.styl'
|
||||
|
||||
.InputDialog
|
||||
.simpleDialog
|
||||
position: relative
|
||||
|
||||
.simpleDialog
|
||||
position: fixed;
|
||||
top: 0px;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,88 @@ 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,
|
||||
})
|
||||
),
|
||||
onDatesChange: PropTypes.func.isRequired,
|
||||
startDate: PropTypes.instanceOf(Date),
|
||||
endDate: PropTypes.instanceOf(Date),
|
||||
month: PropTypes.instanceOf(Date),
|
||||
};
|
||||
|
||||
export default CustomDateRangePicker;
|
||||
@@ -3,11 +3,22 @@ import './StudyList.styl';
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import TableSearchFilter from './TableSearchFilter.js';
|
||||
import useMedia from '../../hooks/useMedia.js';
|
||||
import PropTypes from 'prop-types';
|
||||
import { StudyListLoadingText } from './StudyListLoadingText.js';
|
||||
import { withTranslation } from '../../utils/LanguageProvider';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const getContentFromUseMediaValue = (
|
||||
displaySize,
|
||||
contentArrayMap,
|
||||
defaultContent
|
||||
) => {
|
||||
const content =
|
||||
displaySize in contentArrayMap
|
||||
? contentArrayMap[displaySize]
|
||||
: defaultContent;
|
||||
|
||||
return content;
|
||||
};
|
||||
/**
|
||||
*
|
||||
*
|
||||
@@ -24,10 +35,10 @@ function StudyList(props) {
|
||||
filterValues,
|
||||
onFilterChange: handleFilterChange,
|
||||
onSelectItem: handleSelectItem,
|
||||
t,
|
||||
//
|
||||
studyListDateFilterNumDays,
|
||||
displaySize,
|
||||
} = props;
|
||||
const { t, ready: translationsAreReady } = useTranslation('StudyList');
|
||||
|
||||
const largeTableMeta = [
|
||||
{
|
||||
@@ -70,13 +81,13 @@ function StudyList(props) {
|
||||
|
||||
const mediumTableMeta = [
|
||||
{
|
||||
displayText: 'Patient / MRN',
|
||||
displayText: `${t('PatientName')} / ${t('MRN')}`,
|
||||
fieldName: 'patientNameOrId',
|
||||
inputType: 'text',
|
||||
size: 250,
|
||||
},
|
||||
{
|
||||
displayText: 'Description',
|
||||
displayText: t('Description'),
|
||||
fieldName: 'accessionOrModalityOrDescription',
|
||||
inputType: 'text',
|
||||
size: 350,
|
||||
@@ -91,16 +102,16 @@ function StudyList(props) {
|
||||
|
||||
const smallTableMeta = [
|
||||
{
|
||||
displayText: 'Search',
|
||||
displayText: t('Search'),
|
||||
fieldName: 'allFields',
|
||||
inputType: 'text',
|
||||
size: 100,
|
||||
},
|
||||
];
|
||||
|
||||
const tableMeta = useMedia(
|
||||
['(min-width: 1750px)', '(min-width: 1000px)', '(min-width: 768px)'],
|
||||
[largeTableMeta, mediumTableMeta, smallTableMeta],
|
||||
const tableMeta = getContentFromUseMediaValue(
|
||||
displaySize,
|
||||
{ large: largeTableMeta, medium: mediumTableMeta, small: smallTableMeta },
|
||||
smallTableMeta
|
||||
);
|
||||
|
||||
@@ -108,7 +119,7 @@ function StudyList(props) {
|
||||
.map(field => field.size)
|
||||
.reduce((prev, next) => prev + next);
|
||||
|
||||
return (
|
||||
return translationsAreReady ? (
|
||||
<table className="table table--striped table--hoverable">
|
||||
<colgroup>
|
||||
{tableMeta.map((field, i) => {
|
||||
@@ -176,12 +187,12 @@ function StudyList(props) {
|
||||
studyDate={study.studyDate}
|
||||
studyDescription={study.studyDescription || ''}
|
||||
studyInstanceUid={study.studyInstanceUid}
|
||||
t={t}
|
||||
displaySize={displaySize}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
) : null;
|
||||
}
|
||||
|
||||
StudyList.propTypes = {
|
||||
@@ -206,10 +217,12 @@ StudyList.propTypes = {
|
||||
patientNameOrId: PropTypes.string.isRequired,
|
||||
accessionOrModalityOrDescription: PropTypes.string.isRequired,
|
||||
allFields: PropTypes.string.isRequired,
|
||||
studyDateTo: PropTypes.any,
|
||||
studyDateFrom: PropTypes.any,
|
||||
}).isRequired,
|
||||
onFilterChange: PropTypes.func.isRequired,
|
||||
//
|
||||
studyListDateFilterNumDays: PropTypes.number,
|
||||
displaySize: PropTypes.string,
|
||||
};
|
||||
|
||||
StudyList.defaultProps = {};
|
||||
@@ -225,9 +238,11 @@ function TableRow(props) {
|
||||
studyDescription,
|
||||
studyInstanceUid,
|
||||
onClick: handleClick,
|
||||
t,
|
||||
displaySize,
|
||||
} = props;
|
||||
|
||||
const { t } = useTranslation('StudyList');
|
||||
|
||||
const largeRowTemplate = (
|
||||
<tr
|
||||
onClick={() => handleClick(studyInstanceUid)}
|
||||
@@ -362,9 +377,13 @@ function TableRow(props) {
|
||||
</tr>
|
||||
);
|
||||
|
||||
const rowTemplate = useMedia(
|
||||
['(min-width: 1750px)', '(min-width: 1000px)', '(min-width: 768px)'],
|
||||
[largeRowTemplate, mediumRowTemplate, smallRowTemplate],
|
||||
const rowTemplate = getContentFromUseMediaValue(
|
||||
displaySize,
|
||||
{
|
||||
large: largeRowTemplate,
|
||||
medium: mediumRowTemplate,
|
||||
small: smallRowTemplate,
|
||||
},
|
||||
smallRowTemplate
|
||||
);
|
||||
|
||||
@@ -380,11 +399,11 @@ TableRow.propTypes = {
|
||||
studyDate: PropTypes.string.isRequired,
|
||||
studyDescription: PropTypes.string.isRequired,
|
||||
studyInstanceUid: PropTypes.string.isRequired,
|
||||
displaySize: PropTypes.string,
|
||||
};
|
||||
|
||||
TableRow.defaultProps = {
|
||||
isHighlighted: false,
|
||||
};
|
||||
|
||||
const connectedComponent = withTranslation('StudyList')(StudyList);
|
||||
export { connectedComponent as StudyList };
|
||||
export { StudyList };
|
||||
@@ -176,7 +176,7 @@ table.table
|
||||
margin: 0 auto;
|
||||
color: var(--table-text-primary-color);
|
||||
font-weight: 400;
|
||||
padding: 20px;
|
||||
padding: 20px 8px;
|
||||
user-select: none;
|
||||
font-size: 15px;
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import { Icon } from './../../elements/Icon';
|
||||
// TODO: useTranslation
|
||||
import { withTranslation } from '../../utils/LanguageProvider';
|
||||
import { withTranslation } from '../../contextProviders';
|
||||
|
||||
function StudyListLoadingText({ t: translate }) {
|
||||
return (
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import './PaginationArea.styl';
|
||||
import { withTranslation } from '../../utils/LanguageProvider';
|
||||
import { withTranslation } from '../../contextProviders';
|
||||
|
||||
class TablePagination extends PureComponent {
|
||||
static defaultProps = {
|
||||
|
||||
@@ -6,7 +6,21 @@ import CustomDateRangePicker from './CustomDateRangePicker.js';
|
||||
import { Icon } from './../../elements/Icon';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const getDateEntry = (datePicked, rangeDatePicked) => {
|
||||
return rangeDatePicked || datePicked || null;
|
||||
};
|
||||
|
||||
const getDateEntryFromRange = (today, numOfDays, edge = 'start') => {
|
||||
if (typeof numOfDays !== 'number') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (edge === 'end') {
|
||||
return today;
|
||||
} else {
|
||||
today.subtract(numOfDays, 'days');
|
||||
}
|
||||
};
|
||||
|
||||
function TableSearchFilter(props) {
|
||||
const {
|
||||
@@ -17,20 +31,32 @@ function TableSearchFilter(props) {
|
||||
sortFieldName,
|
||||
sortDirection,
|
||||
// TODO: Rename
|
||||
studyListDateFilterNumDays
|
||||
studyListDateFilterNumDays,
|
||||
} = props;
|
||||
|
||||
const { studyDateTo, studyDateFrom } = values || {};
|
||||
const [focusedInput, setFocusedInput] = useState(null);
|
||||
const [t] = useTranslation(); // 'Common'?
|
||||
const { t, ready: translationsAreReady } = useTranslation('Common');
|
||||
|
||||
const sortIcons = ['sort', 'sort-up', 'sort-down'];
|
||||
const sortIconForSortField =
|
||||
sortDirection === 'asc' ? sortIcons[1] : sortIcons[2];
|
||||
//
|
||||
|
||||
const today = moment();
|
||||
const lastWeek = moment().subtract(7, 'day');
|
||||
const lastMonth = moment().subtract(1, 'month');
|
||||
const defaultStartDate = moment().subtract(studyListDateFilterNumDays, 'days');
|
||||
const defaultEndDate = today;
|
||||
|
||||
const defaultStartDate = getDateEntryFromRange(
|
||||
today,
|
||||
studyListDateFilterNumDays,
|
||||
'start'
|
||||
);
|
||||
const defaultEndDate = getDateEntryFromRange(
|
||||
today,
|
||||
studyListDateFilterNumDays,
|
||||
'end'
|
||||
);
|
||||
|
||||
const studyDatePresets = [
|
||||
{
|
||||
text: t('Today'),
|
||||
@@ -49,56 +75,58 @@ function TableSearchFilter(props) {
|
||||
},
|
||||
];
|
||||
|
||||
return meta.map((field, i) => {
|
||||
const { displayText, fieldName, inputType } = field;
|
||||
const isSortField = sortFieldName === fieldName;
|
||||
const sortIcon = isSortField ? sortIconForSortField : sortIcons[0];
|
||||
return translationsAreReady
|
||||
? meta.map((field, i) => {
|
||||
const { displayText, fieldName, inputType } = field;
|
||||
const isSortField = sortFieldName === fieldName;
|
||||
const sortIcon = isSortField ? sortIconForSortField : sortIcons[0];
|
||||
|
||||
return (
|
||||
<th key={`${fieldName}-${i}`}>
|
||||
<label
|
||||
htmlFor={`filter-${fieldName}`}
|
||||
onClick={() => onSort(fieldName)}
|
||||
>
|
||||
{`${displayText} `}
|
||||
<Icon name={sortIcon} style={{ fontSize: '12px' }} />
|
||||
</label>
|
||||
{inputType === 'text' && (
|
||||
<input
|
||||
type="text"
|
||||
id={`filter-${fieldName}`}
|
||||
className="form-control studylist-search"
|
||||
value={values[fieldName]}
|
||||
onChange={e => onValueChange(fieldName, e.target.value)}
|
||||
/>
|
||||
)}
|
||||
{inputType === 'date-range' && (
|
||||
// https://github.com/airbnb/react-dates
|
||||
<CustomDateRangePicker
|
||||
// Required
|
||||
startDate={studyListDateFilterNumDays ? defaultStartDate : null}
|
||||
startDateId="start-date"
|
||||
endDate={studyListDateFilterNumDays ? defaultEndDate : null}
|
||||
endDateId="end-date"
|
||||
// TODO: We need a dynamic way to determine which fields values to update
|
||||
onDatesChange={({ startDate, endDate, preset = false }) => {
|
||||
onValueChange('studyDateTo', startDate);
|
||||
onValueChange('studyDateFrom', endDate);
|
||||
}}
|
||||
focusedInput={focusedInput}
|
||||
onFocusChange={updatedVal => setFocusedInput(updatedVal)}
|
||||
// Optional
|
||||
numberOfMonths={1} // For med and small screens? 2 for large?
|
||||
showClearDates={true}
|
||||
anchorDirection="left"
|
||||
presets={studyDatePresets}
|
||||
hideKeyboardShortcutsPanel={true}
|
||||
isOutsideRange={day => !isInclusivelyBeforeDay(day, moment())}
|
||||
/>
|
||||
)}
|
||||
</th>
|
||||
);
|
||||
});
|
||||
return (
|
||||
<th key={`${fieldName}-${i}`}>
|
||||
<label
|
||||
htmlFor={`filter-${fieldName}`}
|
||||
onClick={() => onSort(fieldName)}
|
||||
>
|
||||
{`${displayText}`}
|
||||
<Icon name={sortIcon} style={{ fontSize: '12px' }} />
|
||||
</label>
|
||||
{inputType === 'text' && (
|
||||
<input
|
||||
type="text"
|
||||
id={`filter-${fieldName}`}
|
||||
className="form-control studylist-search"
|
||||
value={values[fieldName]}
|
||||
onChange={e => onValueChange(fieldName, e.target.value)}
|
||||
/>
|
||||
)}
|
||||
{inputType === 'date-range' && (
|
||||
// https://github.com/airbnb/react-dates
|
||||
<CustomDateRangePicker
|
||||
// Required
|
||||
startDate={getDateEntry(studyDateFrom, defaultStartDate)}
|
||||
startDateId="start-date"
|
||||
endDate={getDateEntry(studyDateTo, defaultEndDate)}
|
||||
endDateId="end-date"
|
||||
// TODO: We need a dynamic way to determine which fields values to update
|
||||
onDatesChange={({ startDate, endDate, preset = false }) => {
|
||||
onValueChange('studyDateFrom', startDate);
|
||||
onValueChange('studyDateTo', endDate);
|
||||
}}
|
||||
focusedInput={focusedInput}
|
||||
onFocusChange={updatedVal => setFocusedInput(updatedVal)}
|
||||
// Optional
|
||||
numberOfMonths={1} // For med and small screens? 2 for large?
|
||||
showClearDates={true}
|
||||
anchorDirection="left"
|
||||
presets={studyDatePresets}
|
||||
hideKeyboardShortcutsPanel={true}
|
||||
isOutsideRange={day => !isInclusivelyBeforeDay(day, moment())}
|
||||
/>
|
||||
)}
|
||||
</th>
|
||||
);
|
||||
})
|
||||
: null;
|
||||
}
|
||||
|
||||
TableSearchFilter.propTypes = {
|
||||
|
||||
@@ -1,17 +1,57 @@
|
||||
import React, { Component } from 'react';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import LanguageSwitcher from '../languageSwitcher';
|
||||
import i18n from '@ohif/i18n';
|
||||
|
||||
export class GeneralPreferences extends Component {
|
||||
render() {
|
||||
return (
|
||||
<div className="general-preferences-wrapper">
|
||||
<div className="col-sm-3">
|
||||
<label htmlFor="language-select" className="p-r-1">
|
||||
Language
|
||||
</label>
|
||||
<LanguageSwitcher />
|
||||
</div>
|
||||
/**
|
||||
* General Preferences tab
|
||||
*/
|
||||
|
||||
/**
|
||||
* General Preferences tab
|
||||
* It renders the General Preferences content
|
||||
*
|
||||
* It stores current state and whenever it changes, component messages parent of new value (through function callback)
|
||||
* @param {object} props component props
|
||||
* @param {string} props.name Tab`s name
|
||||
* @param {object} props.generalPreferences Data for initial state
|
||||
* @param {function} props.onTabStateChanged Callback function to communicate parent in case its states changes
|
||||
* @param {function} props.onTabErrorChanged Callback Function in case any error on tab
|
||||
*/
|
||||
function GeneralPreferences({
|
||||
generalPreferences,
|
||||
name,
|
||||
onTabStateChanged,
|
||||
onTabErrorChanged,
|
||||
}) {
|
||||
const { language = i18n.language } = generalPreferences;
|
||||
|
||||
const onLanguageChange = language => {
|
||||
onTabStateChanged(name, {
|
||||
generalPreferences: { ...generalPreferences, language },
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="general-preferences-wrapper">
|
||||
<div className="col-sm-3">
|
||||
<label htmlFor="language-select" className="p-r-1">
|
||||
Language
|
||||
</label>
|
||||
<LanguageSwitcher
|
||||
language={language}
|
||||
onLanguageChange={onLanguageChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
GeneralPreferences.propTypes = {
|
||||
generalPreferences: PropTypes.any,
|
||||
name: PropTypes.string,
|
||||
onTabStateChanged: PropTypes.func,
|
||||
onTabErrorChanged: PropTypes.func,
|
||||
};
|
||||
|
||||
export { GeneralPreferences };
|
||||
@@ -1,80 +1,238 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import './HotKeysPreferences.styl';
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import {
|
||||
allowedKeys,
|
||||
disallowedCombinations,
|
||||
specialKeys,
|
||||
} from './hotKeysConfig.js';
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import isEqual from 'lodash.isequal';
|
||||
|
||||
export class HotKeysPreferences extends Component {
|
||||
static propTypes = {
|
||||
hotkeyDefinitions: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
commandName: PropTypes.string,
|
||||
keys: PropTypes.arrayOf(PropTypes.string),
|
||||
label: PropTypes.string,
|
||||
})
|
||||
).isRequired,
|
||||
const getKeysPressedArray = keyDownEvent => {
|
||||
const keysPressedArray = [];
|
||||
const { ctrlKey, altKey, shiftKey } = keyDownEvent;
|
||||
|
||||
if (ctrlKey && !altKey) {
|
||||
keysPressedArray.push('ctrl');
|
||||
}
|
||||
|
||||
if (shiftKey && !altKey) {
|
||||
keysPressedArray.push('shift');
|
||||
}
|
||||
|
||||
if (altKey && !ctrlKey) {
|
||||
keysPressedArray.push('alt');
|
||||
}
|
||||
|
||||
return keysPressedArray;
|
||||
};
|
||||
|
||||
const findConflictingCommand = (
|
||||
originalHotKeys,
|
||||
currentCommandName,
|
||||
currentHotKeys
|
||||
) => {
|
||||
let firstConflictingCommand = undefined;
|
||||
|
||||
for (const commandName in originalHotKeys) {
|
||||
const toolHotKeys = originalHotKeys[commandName].keys;
|
||||
|
||||
if (
|
||||
isEqual(toolHotKeys, currentHotKeys) &&
|
||||
commandName !== currentCommandName
|
||||
) {
|
||||
firstConflictingCommand = originalHotKeys[commandName];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return firstConflictingCommand;
|
||||
};
|
||||
|
||||
/**
|
||||
* Splits given keysObj into arrays. Each array item will be a representation of column
|
||||
* @param {obj} keysObj objects to be splitted into columns
|
||||
* @param {number} columnSize How many rows per column
|
||||
*/
|
||||
const getHotKeysArrayColumns = (keysObj = {}, columnSize) => {
|
||||
if (isNaN(columnSize)) {
|
||||
return keysObj;
|
||||
}
|
||||
|
||||
const keys = Object.keys(keysObj);
|
||||
const keysValues = Object.values(keysObj);
|
||||
const keysLength = keys.length;
|
||||
|
||||
// Columns from left should be bigger;
|
||||
let currentColumn = 0;
|
||||
const dividedKeys = [];
|
||||
|
||||
for (
|
||||
let it = 0;
|
||||
it < keysLength;
|
||||
it++, it % columnSize === 0 ? currentColumn++ : currentColumn
|
||||
) {
|
||||
if (!dividedKeys[currentColumn]) {
|
||||
dividedKeys[currentColumn] = [];
|
||||
}
|
||||
|
||||
dividedKeys[currentColumn][keys[it]] = keysValues[it];
|
||||
}
|
||||
|
||||
return dividedKeys;
|
||||
};
|
||||
|
||||
const NO_FIELD_ERROR_MESSAGE = undefined;
|
||||
const formatPressedKeys = pressedKeysArray => pressedKeysArray.join('+');
|
||||
const unFormatPressedKeys = (pressedKeysStr = '') => pressedKeysStr.split('+');
|
||||
const inputValidators = (
|
||||
commandName,
|
||||
inputValue,
|
||||
pressedKeys,
|
||||
lastPressedKey,
|
||||
originalHotKeys
|
||||
) => {
|
||||
let hasError = false;
|
||||
let errorMessage = NO_FIELD_ERROR_MESSAGE;
|
||||
|
||||
const modifierValidator = ({ lastPressedKey }) => {
|
||||
// Check if it has a valid modifier
|
||||
const isModifier = ['ctrl', 'alt', 'shift'].includes(lastPressedKey);
|
||||
if (isModifier) {
|
||||
hasError = true;
|
||||
errorMessage =
|
||||
"It's not possible to define only modifier keys (ctrl, alt and shift) as a shortcut";
|
||||
return {
|
||||
hasError,
|
||||
errorMessage,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
hotKeys: this.props.hotkeyDefinitions,
|
||||
errorMessages: {},
|
||||
};
|
||||
|
||||
this.onInputKeyDown = this.onInputKeyDown.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes the keys used in a KeyPress event and returns an array of the
|
||||
* keys pressed
|
||||
*
|
||||
* @param {KeyDownEvent} keyDownEvent
|
||||
* @returns {string[]}
|
||||
*/
|
||||
getKeysPressedArray(keyDownEvent) {
|
||||
const keysPressedArray = [];
|
||||
const { ctrlKey, altKey, shiftKey } = keyDownEvent;
|
||||
|
||||
if (ctrlKey && !altKey) {
|
||||
keysPressedArray.push('ctrl');
|
||||
const emptyValidator = ({ inputValue }) => {
|
||||
if (!inputValue) {
|
||||
hasError = true;
|
||||
errorMessage = "Field can't be empty.";
|
||||
return {
|
||||
hasError,
|
||||
errorMessage,
|
||||
};
|
||||
}
|
||||
|
||||
if (shiftKey && !altKey) {
|
||||
keysPressedArray.push('shift');
|
||||
};
|
||||
const conflictingValidator = ({
|
||||
commandName,
|
||||
pressedKeys,
|
||||
originalHotKeys,
|
||||
}) => {
|
||||
const conflictingCommand = findConflictingCommand(
|
||||
originalHotKeys,
|
||||
commandName,
|
||||
pressedKeys
|
||||
);
|
||||
if (conflictingCommand) {
|
||||
hasError = true;
|
||||
errorMessage = `"${conflictingCommand.label}" is already using the "${pressedKeys}" shortcut.`;
|
||||
return {
|
||||
hasError,
|
||||
errorMessage,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
if (altKey && !ctrlKey) {
|
||||
keysPressedArray.push('alt');
|
||||
const disallowedValidator = ({ inputValue, pressedKeys, lastPressedKey }) => {
|
||||
const modifierCommand = formatPressedKeys(
|
||||
pressedKeys.slice(0, pressedKeys.length - 1)
|
||||
);
|
||||
|
||||
const disallowedCombination = disallowedCombinations[modifierCommand];
|
||||
const hasDisallowedCombinations = disallowedCombination
|
||||
? disallowedCombination.includes(lastPressedKey)
|
||||
: false;
|
||||
|
||||
if (hasDisallowedCombinations) {
|
||||
hasError = true;
|
||||
errorMessage = `"${inputValue}" shortcut combination is not allowed`;
|
||||
return {
|
||||
hasError,
|
||||
errorMessage,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
return keysPressedArray;
|
||||
}
|
||||
const validators = [
|
||||
emptyValidator,
|
||||
modifierValidator,
|
||||
conflictingValidator,
|
||||
disallowedValidator,
|
||||
];
|
||||
|
||||
getConflictingCommand(currentCommandName, currentHotKeys) {
|
||||
return this.state.hotKeys.find((tool, index) => {
|
||||
const toolHotKeys = tool.keys[0];
|
||||
return (
|
||||
toolHotKeys &&
|
||||
toolHotKeys === currentHotKeys &&
|
||||
tool.commandName !== currentCommandName
|
||||
);
|
||||
for (const validator of validators) {
|
||||
const validation = validator({
|
||||
commandName,
|
||||
inputValue,
|
||||
pressedKeys,
|
||||
lastPressedKey,
|
||||
originalHotKeys,
|
||||
});
|
||||
if (validation && validation.hasError) {
|
||||
return validation;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {String} commandName
|
||||
* @param {KeyDownEvent} keyDownEvent
|
||||
* @param {Boolean} [displayPressedKey=false]
|
||||
*/
|
||||
updateInputText(commandName, keyDownEvent, displayPressedKey = false) {
|
||||
const pressedKeys = this.getKeysPressedArray(keyDownEvent);
|
||||
// validation has passed successfully
|
||||
return {
|
||||
hasError,
|
||||
errorMessage,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* HotKeysPreferencesRow
|
||||
* Renders row for hotkey preference
|
||||
* It stores current state and whenever it changes, component messages parent of new value (through function callback)
|
||||
* @param {object} props component props
|
||||
* @param {string} props.commandName command name associated to given row
|
||||
* @param {string[]} props.hotkeys keys associated to given command
|
||||
* @param {object} props.originalHotKeys original hotkeys values
|
||||
* @param {function} props.onSuccessChanged Callback function to communicate parent in case its states changes
|
||||
* @param {function} props.onFailureChanged Callback Function in case any error on row
|
||||
*/
|
||||
function HotKeyPreferencesRow({
|
||||
commandName,
|
||||
hotkeys,
|
||||
label,
|
||||
originalHotKeys,
|
||||
tabError,
|
||||
onSuccessChanged,
|
||||
onFailureChanged,
|
||||
}) {
|
||||
const [inputValue, setInputValue] = useState(formatPressedKeys(hotkeys));
|
||||
const [error, setError] = useState(false);
|
||||
|
||||
const [fieldErrorMessage, setFieldErrorMessage] = useState(
|
||||
NO_FIELD_ERROR_MESSAGE
|
||||
);
|
||||
|
||||
// reset error count if tab has no errors
|
||||
useEffect(() => {
|
||||
if (!tabError) {
|
||||
setError(false);
|
||||
setFieldErrorMessage(NO_FIELD_ERROR_MESSAGE);
|
||||
setInputValue(formatPressedKeys(hotkeys));
|
||||
}
|
||||
}, [tabError]);
|
||||
|
||||
// update state values if props changes
|
||||
useEffect(() => {
|
||||
setInputValue(formatPressedKeys(hotkeys));
|
||||
}, [hotkeys]);
|
||||
|
||||
const updateInputText = (keyDownEvent, displayPressedKey = false) => {
|
||||
const pressedKeys = getKeysPressedArray(keyDownEvent);
|
||||
|
||||
if (displayPressedKey) {
|
||||
const specialKeyName = specialKeys[keyDownEvent.which];
|
||||
@@ -82,183 +240,213 @@ export class HotKeysPreferences extends Component {
|
||||
specialKeyName ||
|
||||
keyDownEvent.key ||
|
||||
String.fromCharCode(keyDownEvent.keyCode);
|
||||
|
||||
// ensure lowerCase
|
||||
pressedKeys.push(keyName.toLowerCase());
|
||||
}
|
||||
|
||||
this.updateHotKeysState(commandName, pressedKeys.join('+'));
|
||||
}
|
||||
setInputValue(formatPressedKeys(pressedKeys));
|
||||
};
|
||||
|
||||
updateHotKeysState(commandName, keys) {
|
||||
const hotKeys = this.state.hotKeys;
|
||||
const hotKeyIndex = this.state.hotKeys.findIndex(
|
||||
x => x.commandName === commandName
|
||||
// validate input value
|
||||
const validateInput = () => {
|
||||
const pressedKeys = unFormatPressedKeys(inputValue);
|
||||
const lastPressedKey = pressedKeys[pressedKeys.length - 1];
|
||||
|
||||
const {
|
||||
hasError = false,
|
||||
errorMessage = NO_FIELD_ERROR_MESSAGE,
|
||||
} = inputValidators(
|
||||
commandName,
|
||||
inputValue,
|
||||
pressedKeys,
|
||||
lastPressedKey,
|
||||
originalHotKeys
|
||||
);
|
||||
hotKeys[hotKeyIndex].keys[0] = keys;
|
||||
this.setState({ hotKeys });
|
||||
}
|
||||
|
||||
updateErrorsState(toolKey, errorMessage) {
|
||||
const errorMessages = this.state.errorMessages;
|
||||
errorMessages[toolKey] = errorMessage;
|
||||
this.setState({ errorMessages });
|
||||
}
|
||||
if (hasError) {
|
||||
setInputValue('');
|
||||
} else {
|
||||
onSuccessChanged([inputValue]);
|
||||
}
|
||||
|
||||
onInputKeyDown(event, commandName) {
|
||||
if (hasError !== error) {
|
||||
setError(hasError);
|
||||
}
|
||||
|
||||
setFieldErrorMessage(errorMessage);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
onFailureChanged(error);
|
||||
}, [error]);
|
||||
|
||||
const onInputKeyDown = event => {
|
||||
// Prevent ESC key from propagating and closing the modal
|
||||
if (event.key === 'Escape') {
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
if (allowedKeys.includes(event.keyCode)) {
|
||||
this.updateInputText(commandName, event, true);
|
||||
} else {
|
||||
this.updateInputText(commandName, event, false);
|
||||
}
|
||||
|
||||
updateInputText(event, allowedKeys.includes(event.keyCode));
|
||||
event.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
onChange(event, commandName) {
|
||||
if (event.ctrlKey || event.altKey || event.shiftKey) {
|
||||
return;
|
||||
return (
|
||||
<tr key={commandName}>
|
||||
<td className="text-right p-r-1">{label}</td>
|
||||
<td width="200">
|
||||
<label
|
||||
className={`wrapperLabel ${
|
||||
fieldErrorMessage !== undefined ? 'state-error' : ''
|
||||
} `}
|
||||
data-key="defaultTool"
|
||||
>
|
||||
<input
|
||||
readOnly={true}
|
||||
type="text"
|
||||
value={inputValue}
|
||||
className="form-control hotkey text-center"
|
||||
onKeyDown={onInputKeyDown}
|
||||
onBlur={validateInput}
|
||||
/>
|
||||
<span className="wrapperText" />
|
||||
<span className="errorMessage">{fieldErrorMessage}</span>
|
||||
</label>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
HotKeyPreferencesRow.propTypes = {
|
||||
commandName: PropTypes.string.isRequired,
|
||||
hotkeys: PropTypes.array.isRequired,
|
||||
label: PropTypes.string.isRequired,
|
||||
originalHotKeys: PropTypes.object.isRequired,
|
||||
tabError: PropTypes.bool.isRequired,
|
||||
onSuccessChanged: PropTypes.func.isRequired,
|
||||
onFailureChanged: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
/**
|
||||
* HotKeysPreferences tab
|
||||
* It renders all hotkeys displayed into columns/rows
|
||||
*
|
||||
* It stores current state and whenever it changes, component messages parent of new value (through function callback)
|
||||
* @param {object} props component props
|
||||
* @param {string} props.name Tab`s name
|
||||
* @param {object} props.hotkeyDefinitions Data for initial state
|
||||
* @param {function} props.onTabStateChanged Callback function to communicate parent in case its states changes
|
||||
* @param {function} props.onTabErrorChanged Callback Function in case any error on tab
|
||||
*/
|
||||
function HotKeysPreferences({
|
||||
hotkeyDefinitions,
|
||||
name,
|
||||
tabError,
|
||||
onTabStateChanged,
|
||||
onTabErrorChanged,
|
||||
}) {
|
||||
const [tabState, setTabState] = useState(hotkeyDefinitions);
|
||||
const [tabErrorCounter, setTabErrorCounter] = useState(0);
|
||||
|
||||
const [numColumns] = useState(2);
|
||||
const [columnSize] = useState(() =>
|
||||
Math.ceil(Object.keys(tabState || {}).length / numColumns)
|
||||
);
|
||||
|
||||
const splittedHotKeys = getHotKeysArrayColumns(tabState, columnSize);
|
||||
|
||||
const onHotKeyChanged = (commandName, hotkeyDefinition, keys) => {
|
||||
const newState = {
|
||||
...tabState,
|
||||
[commandName]: { ...hotkeyDefinition, keys },
|
||||
};
|
||||
setTabState(newState);
|
||||
onTabStateChanged(name, { hotkeyDefinitions: newState });
|
||||
};
|
||||
|
||||
const onErrorChanged = (toInc = true) => {
|
||||
const increment = toInc ? 1 : -1;
|
||||
const newValue = tabErrorCounter + increment;
|
||||
if (newValue >= 0) {
|
||||
setTabErrorCounter(newValue);
|
||||
}
|
||||
};
|
||||
|
||||
// reset error count if tab has no errors
|
||||
useEffect(() => {
|
||||
if (!tabError) {
|
||||
setTabErrorCounter(0);
|
||||
// update tab state
|
||||
setTabState({ ...hotkeyDefinitions });
|
||||
}
|
||||
}, [tabError]);
|
||||
|
||||
// tell parent to update its state
|
||||
useEffect(() => {
|
||||
if (tabErrorCounter === 0) {
|
||||
onTabErrorChanged(name, false);
|
||||
}
|
||||
|
||||
const hotKeyIndex = this.state.hotKeys.findIndex(
|
||||
x => x.commandName === commandName
|
||||
);
|
||||
const hotKey = this.state.hotKeys[hotKeyIndex];
|
||||
const keys = hotKey.keys[0];
|
||||
const pressedKeys = keys.split('+');
|
||||
const lastPressedKey = pressedKeys[pressedKeys.length - 1].toLowerCase();
|
||||
if (tabErrorCounter === 1) {
|
||||
onTabErrorChanged(name, true);
|
||||
}
|
||||
}, [tabErrorCounter]);
|
||||
|
||||
// clear the prior errors
|
||||
this.setState({ errorMessages: {} }, () => {
|
||||
// Check if it has a valid modifier
|
||||
const isModifier = ['ctrl', 'alt', 'shift'].includes(lastPressedKey);
|
||||
if (isModifier) {
|
||||
this.updateHotKeysState(commandName, '');
|
||||
this.updateErrorsState(
|
||||
commandName,
|
||||
"It's not possible to define only modifier keys (ctrl, alt and shift) as a shortcut"
|
||||
);
|
||||
return;
|
||||
}
|
||||
// update local state if parent updates
|
||||
useEffect(() => {
|
||||
setTabState({ ...hotkeyDefinitions });
|
||||
}, [hotkeyDefinitions]);
|
||||
|
||||
/*
|
||||
* Check if it has some conflict
|
||||
*/
|
||||
const conflictedCommand = this.getConflictingCommand(commandName, keys);
|
||||
if (conflictedCommand) {
|
||||
this.updateHotKeysState(commandName, '');
|
||||
this.updateErrorsState(
|
||||
commandName,
|
||||
`"${conflictedCommand.label}" is already using the "${keys}" shortcut.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
/*
|
||||
* Check if is a valid combination
|
||||
*/
|
||||
const modifierCommand = pressedKeys
|
||||
.slice(0, pressedKeys.length - 1)
|
||||
.join('+')
|
||||
.toLowerCase();
|
||||
|
||||
const disallowedCombination = disallowedCombinations[modifierCommand];
|
||||
const hasDisallowedCombinations = disallowedCombination
|
||||
? disallowedCombination.includes(lastPressedKey)
|
||||
: false;
|
||||
|
||||
if (hasDisallowedCombinations) {
|
||||
this.updateHotKeysState(commandName, '');
|
||||
this.updateErrorsState(
|
||||
commandName,
|
||||
`"${pressedKeys.join('+')}" shortcut combination is not allowed`
|
||||
);
|
||||
return;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
renderRow({ commandName, label, keys }) {
|
||||
return (
|
||||
<tr key={commandName}>
|
||||
<td className="text-right p-r-1">{label}</td>
|
||||
<td width="200">
|
||||
<label
|
||||
className={`wrapperLabel ${
|
||||
this.state.errorMessages[commandName] !== undefined
|
||||
? 'state-error'
|
||||
: ''
|
||||
} `}
|
||||
ref={input => (this[commandName] = input)}
|
||||
data-key="defaultTool"
|
||||
>
|
||||
<input
|
||||
readOnly={true}
|
||||
type="text"
|
||||
value={keys[0]}
|
||||
vali="true"
|
||||
className="form-control hotkey text-center"
|
||||
onKeyDown={event => this.onInputKeyDown(event, commandName)}
|
||||
onKeyUp={event => this.onChange(event, commandName)}
|
||||
/>
|
||||
<span className="wrapperText" />
|
||||
<span className="errorMessage">
|
||||
{this.state.errorMessages[commandName]}
|
||||
</span>
|
||||
</label>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const halfWayThough = Math.floor(this.state.hotKeys.length / 2);
|
||||
const firstHalfHotkeys = this.state.hotKeys.slice(0, halfWayThough);
|
||||
const secondHalfHotkeys = this.state.hotKeys.slice(
|
||||
halfWayThough,
|
||||
this.state.hotKeys.length
|
||||
);
|
||||
|
||||
return this.state.hotKeys.length > 0 ? (
|
||||
<div className="HotKeysPreferences">
|
||||
{/* <!-- Column 1 --> */}
|
||||
<div className="column">
|
||||
<table className="full-width">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="text-right p-r-1">Function</th>
|
||||
<th className="text-center">Shortcut</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{firstHalfHotkeys.map(hotkeyDefinition =>
|
||||
this.renderRow(hotkeyDefinition)
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{/* <!-- Column 2 --> */}
|
||||
<div className="column">
|
||||
<table className="full-width">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="text-right p-r-1">Function</th>
|
||||
<th className="text-center">Shortcut</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{secondHalfHotkeys.map(hotkeyDefinition =>
|
||||
this.renderRow(hotkeyDefinition)
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<p>{`No hotkeys are configured for this application. Hotkeys can be configured in the application's app-config.js file.`}</p>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="HotKeysPreferences">
|
||||
{splittedHotKeys.length > 0
|
||||
? splittedHotKeys.map((columnHotKeys, index) => {
|
||||
return (
|
||||
<div className="column" key={index}>
|
||||
<table className="full-width">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="text-right p-r-1">Function</th>
|
||||
<th className="text-center">Shortcut</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{Object.entries(columnHotKeys).map(
|
||||
hotkeyDefinitionTuple => (
|
||||
<HotKeyPreferencesRow
|
||||
key={hotkeyDefinitionTuple[0]}
|
||||
commandName={hotkeyDefinitionTuple[0]}
|
||||
hotkeys={hotkeyDefinitionTuple[1].keys}
|
||||
label={hotkeyDefinitionTuple[1].label}
|
||||
originalHotKeys={tabState}
|
||||
tabError={tabError}
|
||||
onSuccessChanged={keys =>
|
||||
onHotKeyChanged(
|
||||
hotkeyDefinitionTuple[0],
|
||||
hotkeyDefinitionTuple[1],
|
||||
keys
|
||||
)
|
||||
}
|
||||
onFailureChanged={onErrorChanged}
|
||||
></HotKeyPreferencesRow>
|
||||
)
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
: null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
HotKeysPreferences.propTypes = {
|
||||
hotkeyDefinitions: PropTypes.any,
|
||||
name: PropTypes.string,
|
||||
tabError: PropTypes.bool,
|
||||
onTabStateChanged: PropTypes.func,
|
||||
onTabErrorChanged: PropTypes.func,
|
||||
};
|
||||
|
||||
export { HotKeysPreferences };
|
||||
@@ -1,7 +1,5 @@
|
||||
.HotKeysPreferences
|
||||
display: flex;
|
||||
margin-right: -15px;
|
||||
margin-left: -15px;
|
||||
|
||||
.column
|
||||
width: 50%;
|
||||
|
||||
@@ -10,7 +10,7 @@ export class UserPreferences extends Component {
|
||||
static defaultProps = {
|
||||
hotkeyDefinitions: [],
|
||||
windowLevelData: {},
|
||||
generalData: {},
|
||||
generalPreferences: {},
|
||||
};
|
||||
|
||||
// TODO: Make this more generic. Tabs should not be restricted to these entries
|
||||
@@ -23,7 +23,8 @@ export class UserPreferences extends Component {
|
||||
})
|
||||
).isRequired,
|
||||
windowLevelData: PropTypes.object.isRequired,
|
||||
generalData: PropTypes.object.isRequired,
|
||||
generalPreferences: PropTypes.object.isRequired,
|
||||
updatePropValue: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
state = {
|
||||
@@ -64,7 +65,10 @@ export class UserPreferences extends Component {
|
||||
return (
|
||||
<form className="form-themed themed">
|
||||
<div className="form-content">
|
||||
<GeneralPreferences generalData={this.props.generalData} />
|
||||
<GeneralPreferences
|
||||
generalPreferences={this.props.generalPreferences}
|
||||
updatePropValue={this.props.updatePropValue}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
@@ -90,8 +94,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={() => {
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
@import './../../design/styles/common/form.styl'
|
||||
@import './../../design/styles/common/navbar.styl'
|
||||
@import './../../design/styles/common/state.styl'
|
||||
@import './../../design/styles/common/global.styl'
|
||||
|
||||
.modal-body
|
||||
overflow: hidden
|
||||
|
||||
.errorMessage
|
||||
color: var(--state-error-text)
|
||||
font-size: 10px
|
||||
text-transform: uppercase;
|
||||
|
||||
.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
|
||||
|
||||
.popover
|
||||
width: 300px
|
||||
@@ -1,92 +1,219 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useSnackbarContext } from '@ohif/ui';
|
||||
|
||||
import './UserPreferencesForm.styl';
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { withTranslation } from '../../utils/LanguageProvider';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import cloneDeep from 'lodash.clonedeep';
|
||||
import isEqual from 'lodash.isequal';
|
||||
import { UserPreferences } from './UserPreferences';
|
||||
// Tabs Component wrapper
|
||||
import { UserPreferencesTabs } from './UserPreferencesTabs';
|
||||
|
||||
class UserPreferencesForm extends Component {
|
||||
// TODO: Make this component more generic to allow things other than W/L and hotkeys...
|
||||
static propTypes = {
|
||||
onClose: PropTypes.func,
|
||||
onSave: PropTypes.func,
|
||||
onResetToDefaults: PropTypes.func,
|
||||
windowLevelData: PropTypes.object,
|
||||
hotkeyDefinitions: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
commandName: PropTypes.string,
|
||||
keys: PropTypes.arrayOf(PropTypes.string),
|
||||
label: PropTypes.string,
|
||||
})
|
||||
).isRequired,
|
||||
t: PropTypes.func,
|
||||
// Tabs
|
||||
import { HotKeysPreferences } from './HotKeysPreferences';
|
||||
import { WindowLevelPreferences } from './WindowLevelPreferences';
|
||||
import { GeneralPreferences } from './GeneralPreferences';
|
||||
|
||||
/**
|
||||
@typedef TabObject
|
||||
@type {Object}
|
||||
@property {string} name Name for given tab
|
||||
@property {ReactComponent} Component React component for given tab.
|
||||
@property {object} props Props State for given tab component
|
||||
@property {boolean} [hidden] To hidden tab or not
|
||||
*/
|
||||
|
||||
/**
|
||||
* Create tabs obj.
|
||||
* @returns {TabObject[]} Array of TabObjs.
|
||||
*/
|
||||
const createTabs = () => {
|
||||
return [
|
||||
{
|
||||
name: 'Hotkeys',
|
||||
Component: HotKeysPreferences,
|
||||
props: {},
|
||||
},
|
||||
{
|
||||
name: 'General',
|
||||
Component: GeneralPreferences,
|
||||
props: {},
|
||||
},
|
||||
{
|
||||
name: 'Window Level',
|
||||
Component: WindowLevelPreferences,
|
||||
props: {},
|
||||
hidden: true,
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
/**
|
||||
* Main form component to render preferences tabs and buttons
|
||||
* @param {object} props component props
|
||||
* @param {string} props.name Tab`s name
|
||||
* @param {object} props.hotkeyDefinitions Hotkeys Data
|
||||
* @param {object} props.windowLevelData Window level data
|
||||
* @param {function} props.onSave Callback function when saving
|
||||
* @param {function} props.onClose Callback function when closing
|
||||
* @param {function} props.onResetToDefaults Callback function when resetting
|
||||
*/
|
||||
function UserPreferencesForm({
|
||||
onClose,
|
||||
onSave,
|
||||
onResetToDefaults,
|
||||
windowLevelData,
|
||||
hotkeyDefinitions,
|
||||
generalPreferences,
|
||||
hotkeysManager,
|
||||
defaultLanguage,
|
||||
hotkeyDefaults,
|
||||
}) {
|
||||
const [tabs, setTabs] = useState(createTabs());
|
||||
|
||||
const createTabsState = (
|
||||
windowLevelData,
|
||||
hotkeyDefinitions,
|
||||
generalPreferences
|
||||
) => {
|
||||
return {
|
||||
Hotkeys: { hotkeyDefinitions },
|
||||
'Window Level': { windowLevelData },
|
||||
General: { generalPreferences },
|
||||
};
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const [tabsState, setTabsState] = useState(
|
||||
createTabsState(windowLevelData, hotkeyDefinitions, generalPreferences)
|
||||
);
|
||||
|
||||
this.state = {
|
||||
windowLevelData: cloneDeep(props.windowLevelData),
|
||||
hotkeyDefinitions: cloneDeep(props.hotkeyDefinitions),
|
||||
};
|
||||
}
|
||||
const [tabsError, setTabsError] = useState(
|
||||
tabs.reduce((acc, tab) => {
|
||||
acc[tab.name] = false;
|
||||
return acc;
|
||||
}, {})
|
||||
);
|
||||
|
||||
save = () => {
|
||||
this.props.onSave({
|
||||
windowLevelData: this.state.windowLevelData,
|
||||
hotkeyDefinitions: this.state.hotkeyDefinitions,
|
||||
const snackbar = useSnackbarContext();
|
||||
|
||||
const { t, ready: translationsAreReady } = useTranslation(
|
||||
'UserPreferencesModal'
|
||||
);
|
||||
|
||||
const onTabStateChanged = (tabName, newState) => {
|
||||
setTabsState({ ...tabsState, [tabName]: newState });
|
||||
};
|
||||
|
||||
const onTabErrorChanged = (tabName, hasError) => {
|
||||
setTabsError({ ...tabsError, [tabName]: hasError });
|
||||
};
|
||||
|
||||
const hasAnyError = () => {
|
||||
return Object.values(tabsError).reduce((acc, value) => acc || value);
|
||||
};
|
||||
|
||||
const onResetPreferences = () => {
|
||||
const defaultHotKeyDefitions = {};
|
||||
|
||||
hotkeyDefaults.map(item => {
|
||||
const { commandName, ...values } = item;
|
||||
defaultHotKeyDefitions[commandName] = { ...values };
|
||||
});
|
||||
|
||||
// update local state
|
||||
setTabsState({
|
||||
...tabsState,
|
||||
Hotkeys: { hotkeyDefinitions: defaultHotKeyDefitions },
|
||||
General: { generalPreferences: { language: defaultLanguage } },
|
||||
});
|
||||
|
||||
// update tabs state
|
||||
setTabs(createTabs(windowLevelData, hotkeyDefinitions, generalPreferences));
|
||||
|
||||
// reset errors
|
||||
setTabsError(
|
||||
tabs.reduce((acc, tab) => {
|
||||
acc[tab.name] = false;
|
||||
return acc;
|
||||
}, {})
|
||||
);
|
||||
|
||||
snackbar.show({
|
||||
message: (
|
||||
<div dangerouslySetInnerHTML={{ __html: t('ResetDefaultMessage') }} />
|
||||
),
|
||||
type: 'info',
|
||||
});
|
||||
};
|
||||
|
||||
componentDidUpdate(prev, next) {
|
||||
const newStateData = {};
|
||||
const onSavePreferences = event => {
|
||||
const toSave = Object.values(tabsState).reduce((acc, tabState) => {
|
||||
return { ...acc, ...tabState };
|
||||
}, {});
|
||||
|
||||
if (!isEqual(prev.windowLevelData, next.windowLevelData)) {
|
||||
newStateData.windowLevelData = prev.windowLevelData;
|
||||
}
|
||||
onSave(toSave);
|
||||
snackbar.show({
|
||||
message: t('SaveMessage'),
|
||||
type: 'success',
|
||||
});
|
||||
};
|
||||
|
||||
if (!isEqual(prev.hotkeyDefinitions, next.hotkeyDefinitions)) {
|
||||
newStateData.hotkeyDefinitions = prev.hotkeyDefinitions;
|
||||
}
|
||||
// update local state if prop values changes
|
||||
useEffect(() => {
|
||||
setTabsState(
|
||||
createTabsState(windowLevelData, hotkeyDefinitions, generalPreferences)
|
||||
);
|
||||
}, [windowLevelData, hotkeyDefinitions, generalPreferences]);
|
||||
|
||||
if (newStateData.hotkeyDefinitions || newStateData.windowLevelData) {
|
||||
this.setState(newStateData);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="UserPreferencesForm">
|
||||
<UserPreferences
|
||||
windowLevelData={this.state.windowLevelData}
|
||||
hotkeyDefinitions={this.state.hotkeyDefinitions}
|
||||
/>
|
||||
<div className="footer">
|
||||
<button
|
||||
className="btn btn-danger pull-left"
|
||||
onClick={this.props.onResetToDefaults}
|
||||
return translationsAreReady ? (
|
||||
<div className="UserPreferencesForm">
|
||||
<UserPreferencesTabs
|
||||
tabs={tabs}
|
||||
tabsState={tabsState}
|
||||
tabsError={tabsError}
|
||||
onTabStateChanged={onTabStateChanged}
|
||||
onTabErrorChanged={onTabErrorChanged}
|
||||
/>
|
||||
<div className="footer">
|
||||
<button
|
||||
className="btn btn-danger pull-left"
|
||||
data-cy="reset-default-btn"
|
||||
onClick={onResetPreferences}
|
||||
>
|
||||
{t('Reset to Defaults')}
|
||||
</button>
|
||||
<div>
|
||||
<div
|
||||
onClick={onClose}
|
||||
data-cy="cancel-btn"
|
||||
className="btn btn-default"
|
||||
>
|
||||
{this.props.t('Reset to Defaults')}
|
||||
</button>
|
||||
<div>
|
||||
<div onClick={this.props.onClose} className="btn btn-default">
|
||||
{this.props.t('Cancel')}
|
||||
</div>
|
||||
<button className="btn btn-primary" onClick={this.save}>
|
||||
{this.props.t('Save')}
|
||||
</button>
|
||||
{t('Cancel')}
|
||||
</div>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
data-cy="save-btn"
|
||||
disabled={hasAnyError()}
|
||||
onClick={onSavePreferences}
|
||||
>
|
||||
{t('Save')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
</div>
|
||||
) : null;
|
||||
}
|
||||
|
||||
const connectedComponent = withTranslation('UserPreferencesForm')(
|
||||
UserPreferencesForm
|
||||
);
|
||||
export { connectedComponent as UserPreferencesForm };
|
||||
export default connectedComponent;
|
||||
UserPreferencesForm.propTypes = {
|
||||
onClose: PropTypes.func,
|
||||
onSave: PropTypes.func,
|
||||
onResetToDefaults: PropTypes.func,
|
||||
windowLevelData: PropTypes.object,
|
||||
hotkeyDefinitions: PropTypes.object,
|
||||
generalPreferences: PropTypes.object,
|
||||
hotkeysManager: PropTypes.object,
|
||||
defaultLanguage: PropTypes.string,
|
||||
hotkeyDefaults: PropTypes.array,
|
||||
};
|
||||
|
||||
export { UserPreferencesForm };
|
||||
@@ -16,7 +16,6 @@
|
||||
.footer
|
||||
display: flex
|
||||
flex-direction: row
|
||||
padding-bottom: 20px
|
||||
justify-content: space-between
|
||||
|
||||
div
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
import React, { useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import './UserPreferencesTabs.styl';
|
||||
|
||||
/**
|
||||
* Render tab component
|
||||
* @param {object} tab TabObject containing tab data
|
||||
* @param {function} onTabStateChanged Callback Function in case tab changes its state
|
||||
* @param {function} onTabErrorChanged Callback Function in case any error on tab
|
||||
*/
|
||||
const renderTab = (
|
||||
tab = {},
|
||||
tabsState,
|
||||
tabsError,
|
||||
onTabStateChanged,
|
||||
onTabErrorChanged
|
||||
) => {
|
||||
const { props, Component, name, hidden = false } = tab;
|
||||
|
||||
const tabState = tabsState[name];
|
||||
const tabError = tabsError[name];
|
||||
|
||||
return !hidden ? (
|
||||
<form className="form-themed themed tabs">
|
||||
<div className="form-content">
|
||||
<Component
|
||||
key={name}
|
||||
{...tabState}
|
||||
{...props}
|
||||
name={name}
|
||||
tabError={tabError}
|
||||
onTabStateChanged={onTabStateChanged}
|
||||
onTabErrorChanged={onTabErrorChanged}
|
||||
></Component>
|
||||
</div>
|
||||
</form>
|
||||
) : null;
|
||||
};
|
||||
|
||||
const renderTabsHeader = (tabs, activeTabIndex, onHeaderChanged) => {
|
||||
return tabs.length > 0
|
||||
? tabs.map((tab, index) => {
|
||||
const { name, hidden = false } = tab;
|
||||
|
||||
const tabClass =
|
||||
index === activeTabIndex ? 'nav-link active' : 'nav-link';
|
||||
return !hidden ? (
|
||||
<li
|
||||
key={name}
|
||||
onClick={() => {
|
||||
onHeaderChanged(index);
|
||||
}}
|
||||
className={tabClass}
|
||||
>
|
||||
<button>{name}</button>
|
||||
</li>
|
||||
) : null;
|
||||
})
|
||||
: null;
|
||||
};
|
||||
/**
|
||||
* Component to render tabs based on currentActiveTabIndex
|
||||
*
|
||||
* In case any tab changes its state this current component tells parent through function callback
|
||||
* @param {object} props Component props
|
||||
*/
|
||||
function UserPreferencesTabs({
|
||||
tabs,
|
||||
tabsState,
|
||||
tabsError,
|
||||
onTabStateChanged,
|
||||
onTabErrorChanged,
|
||||
}) {
|
||||
const [activeTabIndex, setActiveTabIndex] = useState(0);
|
||||
|
||||
return (
|
||||
<div className="UserPreferencesTabs">
|
||||
<div className="UserPreferencesTabs__selector">
|
||||
<div className="dialog-separator-after">
|
||||
<ul className="nav nav-tabs">
|
||||
{renderTabsHeader(tabs, activeTabIndex, setActiveTabIndex)}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{renderTab(
|
||||
tabs[activeTabIndex],
|
||||
tabsState,
|
||||
tabsError,
|
||||
onTabStateChanged,
|
||||
onTabErrorChanged
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
UserPreferencesTabs.propTypes = {
|
||||
tabs: PropTypes.array.isRequired,
|
||||
tabsState: PropTypes.object.isRequired,
|
||||
tabsError: PropTypes.object.isRequired,
|
||||
onTabStateChanged: PropTypes.func.isRequired,
|
||||
onTabErrorChanged: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export { UserPreferencesTabs };
|
||||
@@ -0,0 +1,29 @@
|
||||
@import './../../design/styles/common/form.styl'
|
||||
@import './../../design/styles/common/navbar.styl'
|
||||
@import './../../design/styles/common/state.styl'
|
||||
@import './../../design/styles/common/global.styl'
|
||||
|
||||
.UserPreferencesTabs
|
||||
display: flex
|
||||
flex-direction: column
|
||||
|
||||
&__selector
|
||||
border-bottom: 3px solid black
|
||||
|
||||
.errorMessage
|
||||
color: var(--state-error-text)
|
||||
font-size: 10px
|
||||
text-transform: uppercase;
|
||||
|
||||
.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
|
||||
@@ -1,94 +1,132 @@
|
||||
import React, { Component } from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import './WindowLevelPreferences.styl';
|
||||
/**
|
||||
* WindowLevelPreferencesRow
|
||||
* Renders row for window level preference
|
||||
* It stores current state and whenever it changes, component messages parent of new value (through function callback)
|
||||
* @param {object} props component props
|
||||
* @param {string} props.description description for given preset
|
||||
* @param {number} props.window window value
|
||||
* @param {number} props.level level value
|
||||
* @param {string} props.rowName name of given row to identify it
|
||||
* @param {function} props.onSuccessChanged Callback function to communicate parent in case its states changes
|
||||
*/
|
||||
function WindowLevelPreferencesRow({
|
||||
description,
|
||||
window,
|
||||
level,
|
||||
rowName,
|
||||
onSuccessChanged,
|
||||
// onFailureChanged
|
||||
}) {
|
||||
const [rowState, setRowState] = useState({ description, window, level });
|
||||
|
||||
export class WindowLevelPreferences extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
data: this.props.windowLevelData,
|
||||
};
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
windowLevelData: PropTypes.object.isRequired,
|
||||
onChange: PropTypes.func,
|
||||
const onInputChanged = (event, name) => {
|
||||
const newValue = event.target.value;
|
||||
setRowState({ ...rowState, [name]: newValue });
|
||||
};
|
||||
|
||||
onChange(event, key, field) {
|
||||
const data = this.state.data;
|
||||
const entry = data[key];
|
||||
entry[field] = event.target.value;
|
||||
this.setState({ data });
|
||||
useEffect(() => {
|
||||
onSuccessChanged(rowName, rowState);
|
||||
}, [rowState]);
|
||||
|
||||
if (this.props.onChange) {
|
||||
this.props.onChange(data);
|
||||
}
|
||||
}
|
||||
|
||||
getWLPreferencesRows(key) {
|
||||
const entry = this.state.data[key];
|
||||
const renderTd = (value, name, type) => {
|
||||
return (
|
||||
<tr key={key}>
|
||||
<td className="p-r-1 text-center">{key}</td>
|
||||
<td className="p-r-1">
|
||||
<label className="wrapperLabel">
|
||||
<input
|
||||
value={entry.description}
|
||||
type="text"
|
||||
className="form-control"
|
||||
onChange={event => {
|
||||
this.onChange(event, key, 'description');
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
</td>
|
||||
<td className="p-r-1">
|
||||
<label className="wrapperLabel">
|
||||
<input
|
||||
value={entry.window}
|
||||
type="number"
|
||||
className="form-control"
|
||||
onChange={event => {
|
||||
this.onChange(event, key, 'window');
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
</td>
|
||||
<td className="p-r-1">
|
||||
<label className="wrapperLabel">
|
||||
<input
|
||||
value={entry.level}
|
||||
type="number"
|
||||
className="form-control"
|
||||
onChange={event => {
|
||||
this.onChange(event, key, 'level');
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
</td>
|
||||
</tr>
|
||||
<td className="p-r-1">
|
||||
<label className="wrapperLabel">
|
||||
<input
|
||||
value={value}
|
||||
type={type}
|
||||
className="form-control"
|
||||
onChange={event => {
|
||||
onInputChanged(event, name);
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
</td>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<table className="full-width">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="p-x-1 text-center presetIndex">Preset</th>
|
||||
<th className="p-x-1">Description</th>
|
||||
<th className="p-x-1">Window</th>
|
||||
<th className="p-x-1">Level</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{Object.keys(this.state.data).map(key => {
|
||||
return this.getWLPreferencesRows(key);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<tr key={rowName}>
|
||||
<td className="p-r-1 text-center">{rowName}</td>
|
||||
{renderTd(rowState.description, 'description', 'text')}
|
||||
{renderTd(rowState.window, 'window', 'number')}
|
||||
{renderTd(rowState.level, 'level', 'number')}
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
WindowLevelPreferencesRow.propTypes = {
|
||||
description: PropTypes.string.isRequired,
|
||||
window: PropTypes.number.isRequired,
|
||||
level: PropTypes.number.isRequired,
|
||||
rowName: PropTypes.string.isRequired,
|
||||
onSuccessChanged: PropTypes.func.isRequired,
|
||||
//onFailureChanged: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
/**
|
||||
* WindowLevelPreferences tab
|
||||
* It renders all window level presets
|
||||
*
|
||||
* It stores current state and whenever it changes, component messages parent of new value (through function callback)
|
||||
* @param {object} props component props
|
||||
* @param {string} props.name Tab`s name
|
||||
* @param {object} props.windowLevelData Data for initial state
|
||||
* @param {function} props.onTabStateChanged Callback function to communicate parent in case its states changes
|
||||
*/
|
||||
function WindowLevelPreferences({
|
||||
windowLevelData,
|
||||
name,
|
||||
onTabStateChanged /*onTabErrorChanged*/,
|
||||
}) {
|
||||
const [tabState, setTabState] = useState(windowLevelData);
|
||||
// TODO to be used once error handling is implemented
|
||||
//const [tabError, setTabError] = useState(false);
|
||||
|
||||
const onWindowLevelChanged = (key, state) => {
|
||||
setTabState({ ...tabState, [key]: state });
|
||||
};
|
||||
|
||||
// tell parent to update its state
|
||||
useEffect(() => {
|
||||
onTabStateChanged(name, { windowLevelData: tabState });
|
||||
}, [tabState]);
|
||||
|
||||
return (
|
||||
<table className="full-width">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="p-x-1 text-center presetIndex">Preset</th>
|
||||
<th className="p-x-1">Description</th>
|
||||
<th className="p-x-1">Window</th>
|
||||
<th className="p-x-1">Level</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{Object.keys(tabState).map(objKey => (
|
||||
<WindowLevelPreferencesRow
|
||||
onSuccessChanged={onWindowLevelChanged}
|
||||
rowName={objKey}
|
||||
key={objKey}
|
||||
description={tabState[objKey].description}
|
||||
window={tabState[objKey].window}
|
||||
level={tabState[objKey].level}
|
||||
></WindowLevelPreferencesRow>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
|
||||
WindowLevelPreferences.propTypes = {
|
||||
windowLevelData: PropTypes.object.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
onTabStateChanged: PropTypes.func.isRequired,
|
||||
//onTabErrorChanged: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export { WindowLevelPreferences };
|
||||
@@ -1,4 +1,4 @@
|
||||
@import './UserPreferences.styl'
|
||||
@import './UserPreferencesTabs.styl'
|
||||
|
||||
.presetIndex
|
||||
padding: 0px 10px 0px 10px
|
||||
padding: 0px 10px 0px 10px
|
||||
@@ -1,4 +1,4 @@
|
||||
export { UserPreferences } from './UserPreferences.js';
|
||||
export { UserPreferencesTabs } from './UserPreferencesTabs.js';
|
||||
export { AboutContent } from '../content/aboutContent/AboutContent.js';
|
||||
export { UserPreferencesForm } from './UserPreferencesForm.js';
|
||||
export { GeneralPreferences } from './GeneralPreferences.js';
|
||||
@@ -0,0 +1,242 @@
|
||||
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]);
|
||||
|
||||
/**
|
||||
* UI Dialog
|
||||
*
|
||||
* @typedef {Object} DialogProps
|
||||
* @property {string} id The dialog id.
|
||||
* @property {DialogContent} content The dialog content.
|
||||
* @property {Object} contentProps The dialog content props.
|
||||
* @property {boolean} isDraggable Controls if dialog content is draggable or not.
|
||||
* @property {boolean} showOverlay Controls dialog overlay.
|
||||
* @property {ElementPosition} defaultPosition Specifies the `x` and `y` that the dragged item should start at.
|
||||
* @property {ElementPosition} position If this property is present, the item becomes 'controlled' and is not responsive to user input.
|
||||
* @property {Function} onStart Called when dragging starts. If `false` is returned any handler, the action will cancel.
|
||||
* @property {Function} onStop Called when dragging stops.
|
||||
* @property {Function} onDrag Called while dragging.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Creates a new dialog and return its id.
|
||||
*
|
||||
* @param {DialogProps} props The dialog props.
|
||||
* @returns The new dialog id.
|
||||
*/
|
||||
const create = useCallback(props => {
|
||||
const { id } = props;
|
||||
|
||||
let dialogId = id;
|
||||
if (!dialogId) {
|
||||
dialogId = utils.guid();
|
||||
}
|
||||
|
||||
setDialogs(dialogs => [...dialogs, { ...props, id: dialogId }]);
|
||||
|
||||
return dialogId;
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Dismisses the dialog with a given id.
|
||||
*
|
||||
* @param {Object} props -
|
||||
* @property {string} props.id The dialog id.
|
||||
* @returns void
|
||||
*/
|
||||
const dismiss = useCallback(({ id }) => {
|
||||
setDialogs(dialogs => dialogs.filter(dialog => dialog.id !== id));
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Dismisses all dialogs.
|
||||
*
|
||||
* @returns void
|
||||
*/
|
||||
const dismissAll = () => {
|
||||
setDialogs([]);
|
||||
};
|
||||
|
||||
/**
|
||||
* Indicate if there are no dialogs present.
|
||||
*
|
||||
* @returns True if no dialogs are present.
|
||||
*/
|
||||
const isEmpty = () => dialogs && dialogs.length < 1;
|
||||
|
||||
/**
|
||||
* Moves the dialog to the foreground if clicked.
|
||||
*
|
||||
* @param {string} id The dialog id.
|
||||
* @returns void
|
||||
*/
|
||||
const _bringToFront = id => {
|
||||
setDialogs(dialogs => {
|
||||
const topDialog = dialogs.find(dialog => dialog.id === id);
|
||||
return topDialog
|
||||
? [...dialogs.filter(dialog => dialog.id !== id), topDialog]
|
||||
: [];
|
||||
});
|
||||
};
|
||||
|
||||
const renderDialogs = () =>
|
||||
dialogs.map(dialog => {
|
||||
const {
|
||||
id,
|
||||
content: DialogContent,
|
||||
contentProps,
|
||||
position,
|
||||
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',
|
||||
'SPAN',
|
||||
'LABEL',
|
||||
];
|
||||
if (BLACKLIST.includes(target.tagName.toUpperCase())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (validCallback(onStart)) {
|
||||
return onStart(event);
|
||||
}
|
||||
}}
|
||||
onStop={event => {
|
||||
setIsDragging(false);
|
||||
|
||||
if (validCallback(onStop)) {
|
||||
return onStop(event);
|
||||
}
|
||||
}}
|
||||
onDrag={event => {
|
||||
setIsDragging(true);
|
||||
_bringToFront(id);
|
||||
_updateLastDialogPosition(id);
|
||||
|
||||
if (validCallback(onDrag)) {
|
||||
return onDrag(event);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div
|
||||
id={`draggableItem-${id}`}
|
||||
className={classNames('DraggableItem', isDragging && 'dragging')}
|
||||
style={{ zIndex: '999', position: 'absolute' }}
|
||||
onClick={() => _bringToFront(id)}
|
||||
>
|
||||
<DialogContent {...dialog} {...contentProps} />
|
||||
</div>
|
||||
</Draggable>
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* Update the last dialog position to be used as the new default position.
|
||||
*
|
||||
* @returns void
|
||||
*/
|
||||
const _updateLastDialogPosition = dialogId => {
|
||||
const draggableItemBounds = document
|
||||
.querySelector(`#draggableItem-${dialogId}`)
|
||||
.getBoundingClientRect();
|
||||
setLastDialogPosition({
|
||||
x: draggableItemBounds.x,
|
||||
y: draggableItemBounds.y,
|
||||
});
|
||||
};
|
||||
|
||||
const validCallback = callback => callback && typeof callback === 'function';
|
||||
|
||||
return (
|
||||
<DialogContext.Provider value={{ create, dismiss, dismissAll, isEmpty }}>
|
||||
<div className="DraggableArea">
|
||||
{dialogs.some(dialog => dialog.showOverlay) ? (
|
||||
<div className="Overlay active">{renderDialogs()}</div>
|
||||
) : (
|
||||
renderDialogs()
|
||||
)}
|
||||
</div>
|
||||
{children}
|
||||
</DialogContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* High Order Component to use the dialog methods through a Class Component
|
||||
*
|
||||
*/
|
||||
export const withDialog = Component => {
|
||||
return function WrappedComponent(props) {
|
||||
const { create, dismiss, dismissAll, isEmpty } = useDialog();
|
||||
return (
|
||||
<Component {...props} dialog={{ create, dismiss, dismissAll, isEmpty }} />
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
DialogProvider.defaultProps = {
|
||||
service: null,
|
||||
};
|
||||
|
||||
DialogProvider.propTypes = {
|
||||
children: PropTypes.oneOfType([
|
||||
PropTypes.arrayOf(PropTypes.node),
|
||||
PropTypes.node,
|
||||
PropTypes.func,
|
||||
]).isRequired,
|
||||
service: PropTypes.shape({
|
||||
setServiceImplementation: PropTypes.func,
|
||||
}),
|
||||
};
|
||||
|
||||
export default DialogProvider;
|
||||
@@ -0,0 +1,22 @@
|
||||
.DraggableItem
|
||||
div
|
||||
cursor: grab !important
|
||||
|
||||
.DraggableItem.dragging
|
||||
div
|
||||
cursor: grabbing !important
|
||||
|
||||
.DraggableArea, .Overlay
|
||||
width: 100%
|
||||
height: 100%
|
||||
position: absolute
|
||||
|
||||
.Overlay.active
|
||||
position: fixed
|
||||
z-index: 999
|
||||
left: 0
|
||||
top: 0
|
||||
width: 100%
|
||||
height: 100%
|
||||
overflow: auto
|
||||
background: rgba(0,0,0,.1)
|
||||
File renamed without changes.
+53
-41
@@ -13,17 +13,28 @@ const { Provider } = ModalContext;
|
||||
|
||||
export const useModal = () => useContext(ModalContext);
|
||||
|
||||
/**
|
||||
* UI Modal
|
||||
*
|
||||
* @typedef {Object} ModalProps
|
||||
* @property {ReactElement|HTMLElement} [content=null] Modal content.
|
||||
* @property {Object} [contentProps=null] Modal content props.
|
||||
* @property {boolean} [shouldCloseOnEsc=false] Modal is dismissible via the esc key.
|
||||
* @property {boolean} [isOpen=true] Make the Modal visible or hidden.
|
||||
* @property {boolean} [closeButton=true] Should the modal body render the close button.
|
||||
* @property {string} [title=null] Should the modal render the title independently of the body content.
|
||||
* @property {string} [customClassName=null] The custom class to style the modal.
|
||||
*/
|
||||
|
||||
const ModalProvider = ({ children, modal: Modal, service }) => {
|
||||
const DEFAULT_OPTIONS = {
|
||||
component: null /* The component instance inside the modal. */,
|
||||
header: null /* The content inside the modal header. */,
|
||||
footer: null /* The content inside the modal footer. */,
|
||||
backdrop: false /* Should the modal render a backdrop overlay. */,
|
||||
keyboard: false /* Modal is dismissible via the esc key. */,
|
||||
show: true /* Make the Modal visible or hidden. */,
|
||||
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. */,
|
||||
content: null,
|
||||
contentProps: null,
|
||||
shouldCloseOnEsc: false,
|
||||
isOpen: true,
|
||||
closeButton: true,
|
||||
title: null,
|
||||
customClassName: '',
|
||||
};
|
||||
|
||||
const [options, setOptions] = useState(DEFAULT_OPTIONS);
|
||||
@@ -42,13 +53,12 @@ const ModalProvider = ({ children, modal: Modal, service }) => {
|
||||
/**
|
||||
* Show the modal and override its configuration props.
|
||||
*
|
||||
* @param {ModalProps} props { content, contentProps, shouldCloseOnEsc, isOpen, closeButton, title, customClassName }
|
||||
* @returns void
|
||||
*/
|
||||
const show = useCallback(
|
||||
(component, props = {}) =>
|
||||
setOptions(Object.assign({}, options, props, { component })),
|
||||
[options]
|
||||
);
|
||||
const show = useCallback(props => setOptions({ ...options, ...props }), [
|
||||
options,
|
||||
]);
|
||||
|
||||
/**
|
||||
* Hide the modal and set its properties to default.
|
||||
@@ -59,26 +69,28 @@ const ModalProvider = ({ children, modal: Modal, service }) => {
|
||||
DEFAULT_OPTIONS,
|
||||
]);
|
||||
|
||||
const { component: Component } = options;
|
||||
const {
|
||||
content: ModalContent,
|
||||
contentProps,
|
||||
isOpen,
|
||||
title,
|
||||
customClassName,
|
||||
shouldCloseOnEsc,
|
||||
closeButton,
|
||||
} = options;
|
||||
|
||||
return (
|
||||
<Provider value={{ show, hide }}>
|
||||
{options.component && (
|
||||
{ModalContent && (
|
||||
<Modal
|
||||
className={classNames(
|
||||
options.customClassName,
|
||||
options.component.className
|
||||
)}
|
||||
backdrop={options.backdrop}
|
||||
keyboard={options.keyboard}
|
||||
show={options.show}
|
||||
title={options.title}
|
||||
closeButton={options.closeButton}
|
||||
footer={options.footer}
|
||||
header={options.header}
|
||||
onHide={hide}
|
||||
className={classNames(customClassName, ModalContent.className)}
|
||||
shouldCloseOnEsc={shouldCloseOnEsc}
|
||||
isOpen={isOpen}
|
||||
title={title}
|
||||
closeButton={closeButton}
|
||||
onClose={hide}
|
||||
>
|
||||
<Component {...options} show={show} hide={hide} />
|
||||
<ModalContent {...contentProps} show={show} hide={hide} />
|
||||
</Modal>
|
||||
)}
|
||||
{children}
|
||||
@@ -86,6 +98,18 @@ const ModalProvider = ({ children, modal: Modal, service }) => {
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Higher Order Component to use the modal methods through a Class Component.
|
||||
*
|
||||
* @returns
|
||||
*/
|
||||
export const withModal = Component => {
|
||||
return function WrappedComponent(props) {
|
||||
const { show, hide } = useModal();
|
||||
return <Component {...props} modal={{ show, hide }} />;
|
||||
};
|
||||
};
|
||||
|
||||
ModalProvider.defaultProps = {
|
||||
service: null,
|
||||
};
|
||||
@@ -105,18 +129,6 @@ ModalProvider.propTypes = {
|
||||
}),
|
||||
};
|
||||
|
||||
/**
|
||||
* Higher Order Component to use the modal methods through a Class Component.
|
||||
*
|
||||
* @returns
|
||||
*/
|
||||
export const withModal = Component => {
|
||||
return function WrappedComponent(props) {
|
||||
const { show, hide } = useModal();
|
||||
return <Component {...props} modal={{ show, hide }} />;
|
||||
};
|
||||
};
|
||||
|
||||
export default ModalProvider;
|
||||
|
||||
export const ModalConsumer = ModalContext.Consumer;
|
||||
File renamed without changes.
@@ -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';
|
||||
@@ -46,7 +46,7 @@ class DropdownMenu extends Component {
|
||||
<button
|
||||
key={key}
|
||||
className="dd-item"
|
||||
data-cy="about-item-menu"
|
||||
data-cy="dd-item-menu"
|
||||
onClick={() => this.handleOnClick(onClick)}
|
||||
>
|
||||
{icon && <Icon {...icon} className="dd-item-icon" />}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import useMedia from './useMedia.js';
|
||||
import { useMedia } from './useMedia.js';
|
||||
import useDebounce from './useDebounce.js';
|
||||
|
||||
export { useDebounce, useMedia };
|
||||
@@ -1,54 +1,178 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import isEqual from 'lodash.isequal';
|
||||
/**
|
||||
* Get display size value for matched mediaQueryList
|
||||
* @param {MediaQueryList[]} mediaQueryMap - Array of mappings, containing MediaQueryLists
|
||||
* @param {Array} mediaTypesAliases - Array of strings representing each mediaQueryAlias.
|
||||
* @param {string} defaultDisplaySize - default display size value. Fallback value.
|
||||
*/
|
||||
const getDisplaySize = (
|
||||
mediaQueryMap,
|
||||
mediaTypesAliases,
|
||||
defaultDisplaySize
|
||||
) => {
|
||||
if ((!mediaTypesAliases && !defaultDisplaySize) || !mediaQueryMap) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get index of first media query that matches
|
||||
const index = mediaQueryMap.findIndex(mql => mql.matches);
|
||||
|
||||
// Return related value or defaultDisplaySize if none
|
||||
return index >= 0 && typeof mediaTypesAliases[index] !== 'undefined'
|
||||
? mediaTypesAliases[index]
|
||||
: defaultDisplaySize;
|
||||
};
|
||||
/**
|
||||
* Map each window MediaQueryLists
|
||||
* @param {Array} mediaQueriesStringList - array of string media queries to be parsed
|
||||
*/
|
||||
const getMediaQueryMap = mediaQueriesStringList => {
|
||||
return (
|
||||
mediaQueriesStringList &&
|
||||
mediaQueriesStringList.map(q => window.matchMedia(q))
|
||||
);
|
||||
};
|
||||
|
||||
const getMediaTypeAlias = (mediaQuery, state) => {
|
||||
const { media } = mediaQuery;
|
||||
const { mediaQueriesStringList, mediaTypesAliases } = state;
|
||||
|
||||
const index = mediaQueriesStringList.findIndex(originalMediaQuery => {
|
||||
const { media: toCompareMedia } = window.matchMedia(originalMediaQuery);
|
||||
return toCompareMedia === media;
|
||||
});
|
||||
|
||||
return mediaTypesAliases[index];
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to get current displaySize value.
|
||||
*
|
||||
* Its state changes and also displaySize value changes in case viewport is resized.
|
||||
* Its state changes in case mediaQueriesStringList or mediaTypesAliases changes.
|
||||
*
|
||||
* Current hook only offers displayMedia size, it wont expose method to change its state.
|
||||
* @param {Array} mediaQueriesStringList - array of string media queries to be parsed
|
||||
* @param {Array} mediaTypesAliases - array of aliases. Each value represents one mediaQueryList from array mediaQueriesStringList
|
||||
* @param {String} defaultMediaType - default mediaTypeAlias
|
||||
* @returns {String} current displayMedia size based on viewport size.
|
||||
*
|
||||
* @example <caption>Example to getDisplayMedia Size based on viewport size</caption>
|
||||
*
|
||||
* const displaySize = useMedia(
|
||||
* ['(min-width: 1500px)', '(min-width: 1000px)', '(min-width: 600px)'],
|
||||
* // Value to return for matched media query
|
||||
* ['large', 'medium', 'small'],
|
||||
* // Default value
|
||||
* 'medium');
|
||||
*
|
||||
* const currentDisplaySize = useMedia();
|
||||
*
|
||||
* @example <caption></caption>
|
||||
* const currentViewportSize = useMedia(
|
||||
* // Media queries
|
||||
* ['(min-width: 1500px)', '(min-width: 1000px)', '(min-width: 600px)'],
|
||||
* // Value to return for matched media query
|
||||
* ['large', 'medium', 'small'],
|
||||
* // Default value
|
||||
* 'medium'
|
||||
* );
|
||||
* @param {string[]} queries
|
||||
* @param {*} values
|
||||
* @param {*} defaultValue
|
||||
* @returns
|
||||
*/
|
||||
function useMedia(queries, values, defaultValue) {
|
||||
// Array containing a media query list for each query
|
||||
const mediaQueryLists = queries.map(q => window.matchMedia(q));
|
||||
const useMedia = (
|
||||
mediaQueriesStringList,
|
||||
mediaTypesAliases,
|
||||
defaultMediaType
|
||||
) => {
|
||||
// MediaQuery.state is the source of truth. This hook will be dependent on it.
|
||||
const [state, setState] = useState(() => {
|
||||
const _mediaQueryMap = getMediaQueryMap(mediaQueriesStringList);
|
||||
const _displaySize = getDisplaySize(
|
||||
_mediaQueryMap,
|
||||
mediaTypesAliases,
|
||||
defaultMediaType
|
||||
);
|
||||
|
||||
// Function that gets value based on matching media query
|
||||
const getValue = () => {
|
||||
// Get index of first media query that matches
|
||||
const index = mediaQueryLists.findIndex(mql => mql.matches);
|
||||
return {
|
||||
mediaQueryMap: _mediaQueryMap,
|
||||
displaySize: _displaySize,
|
||||
mediaQueriesStringList,
|
||||
mediaTypesAliases,
|
||||
defaultMediaType,
|
||||
};
|
||||
});
|
||||
let mount = useRef(false);
|
||||
|
||||
// Return related value or defaultValue if none
|
||||
return typeof values[index] !== 'undefined' ? values[index] : defaultValue;
|
||||
const updateDisplaySize = displaySize => {
|
||||
if (mount.current) {
|
||||
setState({ ...state, displaySize });
|
||||
}
|
||||
};
|
||||
|
||||
// State and setter for matched value
|
||||
const [value, setValue] = useState(getValue);
|
||||
const updateState = value => {
|
||||
const {
|
||||
mediaQueriesStringList,
|
||||
mediaTypesAliases,
|
||||
defaultMediaType,
|
||||
} = value;
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
// Event listener callback
|
||||
// Note: By defining getValue outside of useEffect we ensure that it has ...
|
||||
// ... current values of hook args (as this hook callback is created once on mount).
|
||||
const handler = () => setValue(getValue);
|
||||
const mediaQueryMap = getMediaQueryMap(mediaQueriesStringList);
|
||||
const displaySize = getDisplaySize(
|
||||
mediaQueryMap,
|
||||
mediaTypesAliases,
|
||||
defaultMediaType
|
||||
);
|
||||
// immutable state
|
||||
// last chance to avoid setState of unmount component
|
||||
if (mount.current) {
|
||||
setState({
|
||||
...state,
|
||||
mediaQueriesStringList,
|
||||
mediaTypesAliases,
|
||||
displaySize,
|
||||
mediaQueryMap,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Set a listener for each media query with above handler as callback.
|
||||
mediaQueryLists.forEach(mql => mql.addListener(handler));
|
||||
const onMediaQueryChange = useCallback(mediaQuery => {
|
||||
if (mediaQuery.matches) {
|
||||
const nextDisplaySize = getMediaTypeAlias(mediaQuery, state);
|
||||
updateDisplaySize(nextDisplaySize);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Remove listeners on cleanup
|
||||
return () => mediaQueryLists.forEach(mql => mql.removeListener(handler));
|
||||
},
|
||||
[] // Empty array ensures effect is only run on mount and unmount
|
||||
);
|
||||
// update state of MediaQuery in case mediaQueriesStringList or mediaTypesAliases has changed
|
||||
useEffect(() => {
|
||||
const {
|
||||
mediaQueriesStringList: _mediaQueriesStringList,
|
||||
mediaTypesAliases: _mediaTypesAliases,
|
||||
} = state;
|
||||
if (
|
||||
(mediaQueriesStringList &&
|
||||
!isEqual(mediaQueriesStringList, _mediaQueriesStringList)) ||
|
||||
(mediaTypesAliases && !isEqual(mediaTypesAliases, _mediaTypesAliases))
|
||||
) {
|
||||
updateState({
|
||||
mediaQueriesStringList,
|
||||
mediaTypesAliases,
|
||||
});
|
||||
}
|
||||
}, [mediaQueriesStringList, mediaTypesAliases]);
|
||||
|
||||
return value;
|
||||
}
|
||||
// re-assign window resizing listeners
|
||||
useEffect(() => {
|
||||
const { mediaQueryMap } = state;
|
||||
mediaQueryMap.forEach(mql => {
|
||||
mql.removeListener(onMediaQueryChange);
|
||||
mql.addListener(onMediaQueryChange);
|
||||
});
|
||||
}, [state.mediaQueryMap]);
|
||||
|
||||
export default useMedia;
|
||||
useEffect(() => {
|
||||
mount.current = true;
|
||||
|
||||
return () => {
|
||||
mount.current = false;
|
||||
const { mediaQueryMap } = state;
|
||||
mediaQueryMap.forEach(mql => {
|
||||
mql.removeListener(onMediaQueryChange);
|
||||
});
|
||||
};
|
||||
}, []);
|
||||
|
||||
return state.displaySize;
|
||||
};
|
||||
|
||||
export { useMedia };
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -3,6 +3,91 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [2.11.2](https://github.com/OHIF/Viewers/compare/@ohif/viewer@2.11.1...@ohif/viewer@2.11.2) (2019-11-28)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* User Preferences Issues ([#1207](https://github.com/OHIF/Viewers/issues/1207)) ([1df21a9](https://github.com/OHIF/Viewers/commit/1df21a9e075b5e6dfc10a429ae825826f46c71b8)), closes [#1161](https://github.com/OHIF/Viewers/issues/1161) [#1164](https://github.com/OHIF/Viewers/issues/1164) [#1177](https://github.com/OHIF/Viewers/issues/1177) [#1179](https://github.com/OHIF/Viewers/issues/1179) [#1180](https://github.com/OHIF/Viewers/issues/1180) [#1181](https://github.com/OHIF/Viewers/issues/1181) [#1182](https://github.com/OHIF/Viewers/issues/1182) [#1183](https://github.com/OHIF/Viewers/issues/1183) [#1184](https://github.com/OHIF/Viewers/issues/1184) [#1185](https://github.com/OHIF/Viewers/issues/1185)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.11.1](https://github.com/OHIF/Viewers/compare/@ohif/viewer@2.11.0...@ohif/viewer@2.11.1) (2019-11-27)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* of undefined name of project ([#1231](https://github.com/OHIF/Viewers/issues/1231)) ([e34a057](https://github.com/OHIF/Viewers/commit/e34a05726319e3e70279c43d5bf976d33cdf71f7))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [2.11.0](https://github.com/OHIF/Viewers/compare/@ohif/viewer@2.10.2...@ohif/viewer@2.11.0) (2019-11-25)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add new annotate tool using new dialog service ([#1211](https://github.com/OHIF/Viewers/issues/1211)) ([8fd3af1](https://github.com/OHIF/Viewers/commit/8fd3af1e137e793f1b482760a22591c64a072047))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.10.2](https://github.com/OHIF/Viewers/compare/@ohif/viewer@2.10.1...@ohif/viewer@2.10.2) (2019-11-25)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Issue branch from danny experimental changes pr 1128 ([#1150](https://github.com/OHIF/Viewers/issues/1150)) ([a870b3c](https://github.com/OHIF/Viewers/commit/a870b3cc6056cf824af422e46f1ad674910b534e)), closes [#1161](https://github.com/OHIF/Viewers/issues/1161) [#1164](https://github.com/OHIF/Viewers/issues/1164) [#1177](https://github.com/OHIF/Viewers/issues/1177) [#1179](https://github.com/OHIF/Viewers/issues/1179) [#1180](https://github.com/OHIF/Viewers/issues/1180) [#1181](https://github.com/OHIF/Viewers/issues/1181) [#1182](https://github.com/OHIF/Viewers/issues/1182) [#1183](https://github.com/OHIF/Viewers/issues/1183) [#1184](https://github.com/OHIF/Viewers/issues/1184) [#1185](https://github.com/OHIF/Viewers/issues/1185)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.10.1](https://github.com/OHIF/Viewers/compare/@ohif/viewer@2.10.0...@ohif/viewer@2.10.1) (2019-11-20)
|
||||
|
||||
**Note:** Version bump only for package @ohif/viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [2.10.0](https://github.com/OHIF/Viewers/compare/@ohif/viewer@2.9.0...@ohif/viewer@2.10.0) (2019-11-19)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* New dialog service ([#1202](https://github.com/OHIF/Viewers/issues/1202)) ([f65639c](https://github.com/OHIF/Viewers/commit/f65639c2b0dab01decd20cab2cef4263cb4fab37))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [2.9.0](https://github.com/OHIF/Viewers/compare/@ohif/viewer@2.8.5...@ohif/viewer@2.9.0) (2019-11-19)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Issue 879 viewer route query param not filtering but promoting ([#1141](https://github.com/OHIF/Viewers/issues/1141)) ([b17f753](https://github.com/OHIF/Viewers/commit/b17f753e6222045252ef885e40233681541a32e1)), closes [#1118](https://github.com/OHIF/Viewers/issues/1118)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.8.5](https://github.com/OHIF/Viewers/compare/@ohif/viewer@2.8.4...@ohif/viewer@2.8.5) (2019-11-18)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* minor date picker UX improvements ([813ee5e](https://github.com/OHIF/Viewers/commit/813ee5ed4d78b7bda234922d5f3389efe346451c))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.8.4](https://github.com/OHIF/Viewers/compare/@ohif/viewer@2.8.3...@ohif/viewer@2.8.4) (2019-11-15)
|
||||
|
||||
**Note:** Version bump only for package @ohif/viewer
|
||||
|
||||
@@ -85,6 +85,7 @@ describe('OHIF Cornerstone Hotkeys', () => {
|
||||
cy.get('@layoutBtn').click();
|
||||
//Select 3 viewports
|
||||
cy.get('tbody > :nth-child(1) > :nth-child(3)').click();
|
||||
cy.waitViewportImageLoading();
|
||||
|
||||
// Press multiples hotkeys on viewport #1
|
||||
cy.get('body').type('VL+++I');
|
||||
|
||||
@@ -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];
|
||||
|
||||
|
||||
@@ -270,10 +270,10 @@ describe('OHIF Study Viewer Page', function() {
|
||||
cy.get('[data-cy="options-menu"]')
|
||||
.first()
|
||||
.click();
|
||||
cy.get('[data-cy="about-item-menu"]')
|
||||
cy.get('[data-cy="dd-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');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,600 @@
|
||||
describe('OHIF User Preferences', () => {
|
||||
context('Study List Page', function() {
|
||||
before(() => {
|
||||
cy.visit('/');
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
// Open User Preferences modal
|
||||
cy.openPreferences();
|
||||
cy.initPreferencesModalAliases();
|
||||
});
|
||||
|
||||
it('checks displayed information on User Preferences modal', function() {
|
||||
cy.get('@preferencesModal').should('contain.text', 'User Preferences');
|
||||
cy.get('@userPreferencesHotkeysTab')
|
||||
.should('have.text', 'Hotkeys')
|
||||
.and('have.class', 'active');
|
||||
cy.get('@userPreferencesGeneralTab').should('have.text', 'General');
|
||||
cy.get('@restoreBtn')
|
||||
.scrollIntoView()
|
||||
.should('have.text', 'Reset to Defaults');
|
||||
cy.get('@cancelBtn').should('have.text', 'Cancel');
|
||||
cy.get('@saveBtn').should('have.text', 'Save');
|
||||
|
||||
// Visual comparison
|
||||
cy.screenshot(
|
||||
'User Preferences Modal - Hotkeys tab initial state in Study Viewer page'
|
||||
);
|
||||
cy.percyCanvasSnapshot(
|
||||
'User Preferences Modal - Hotkeys tab initial state in Study Viewer page'
|
||||
);
|
||||
cy.get('[data-cy="close-button"]').click();
|
||||
});
|
||||
|
||||
it('checks translation by selecting Spanish language', function() {
|
||||
cy.get('@userPreferencesGeneralTab')
|
||||
.click()
|
||||
.should('have.class', 'active');
|
||||
|
||||
// Language dropdown should be displayed
|
||||
cy.get('#language-select').should('be.visible');
|
||||
|
||||
// Visual comparison
|
||||
cy.screenshot(
|
||||
'User Preferences Modal - General tab initial state in Study Viewer page'
|
||||
);
|
||||
cy.percyCanvasSnapshot(
|
||||
'User Preferences Modal - General tab initial state in Study Viewer page'
|
||||
);
|
||||
// Select Spanish and Save
|
||||
cy.get('#language-select').select('Spanish');
|
||||
cy.get('@saveBtn')
|
||||
.scrollIntoView()
|
||||
.click();
|
||||
|
||||
// Header should be translated to Spanish
|
||||
cy.get('.research-use')
|
||||
.scrollIntoView()
|
||||
.should('have.text', 'SOLO USO PARA INVESTIGACIÓN');
|
||||
|
||||
// Options menu should be translated
|
||||
cy.get('[data-cy="options-menu"]')
|
||||
.should('have.text', 'Opciones')
|
||||
.click();
|
||||
|
||||
cy.get('[data-cy="dd-item-menu"]')
|
||||
.first()
|
||||
.should('contain.text', 'Acerca de');
|
||||
cy.get('[data-cy="dd-item-menu"]')
|
||||
.last()
|
||||
.should('contain.text', 'Preferencias');
|
||||
|
||||
// Close Options menu
|
||||
cy.get('[data-cy="options-menu"]').click();
|
||||
});
|
||||
|
||||
it('checks if user can cancel the language selection and application will be in English', function() {
|
||||
// Set language to English and save
|
||||
cy.setLanguage('English');
|
||||
|
||||
// Set language to Spanish and cancel
|
||||
cy.setLanguage('Spanish', false);
|
||||
|
||||
// Header should be kept in English
|
||||
cy.get('.research-use')
|
||||
.scrollIntoView()
|
||||
.should('have.text', 'INVESTIGATIONAL USE ONLY');
|
||||
|
||||
// Options menu should be translated
|
||||
cy.get('[data-cy="options-menu"]')
|
||||
.should('have.text', 'Options')
|
||||
.click();
|
||||
|
||||
cy.get('[data-cy="dd-item-menu"]')
|
||||
.first()
|
||||
.should('contain.text', 'About');
|
||||
cy.get('[data-cy="dd-item-menu"]')
|
||||
.last()
|
||||
.should('contain.text', 'Preferences');
|
||||
|
||||
// Close Options menu
|
||||
cy.get('[data-cy="options-menu"]').click();
|
||||
});
|
||||
|
||||
it('checks if user can restore to default the language selection and application will be in English', function() {
|
||||
// Set language to Spanish
|
||||
cy.setLanguage('Spanish');
|
||||
|
||||
//Open Preferences again
|
||||
cy.openPreferences();
|
||||
|
||||
// Go to general tab
|
||||
cy.get('@userPreferencesGeneralTab').click();
|
||||
|
||||
cy.get('@restoreBtn')
|
||||
.scrollIntoView()
|
||||
.click();
|
||||
|
||||
// Save
|
||||
cy.get('@saveBtn')
|
||||
.scrollIntoView()
|
||||
.click();
|
||||
|
||||
// Header should be in English
|
||||
cy.get('.research-use')
|
||||
.scrollIntoView()
|
||||
.should('have.text', 'INVESTIGATIONAL USE ONLY');
|
||||
|
||||
// Options menu should be in English
|
||||
cy.get('[data-cy="options-menu"]')
|
||||
.should('have.text', 'Options')
|
||||
.click();
|
||||
|
||||
cy.get('[data-cy="dd-item-menu"]')
|
||||
.first()
|
||||
.should('contain.text', 'About');
|
||||
cy.get('[data-cy="dd-item-menu"]')
|
||||
.last()
|
||||
.should('contain.text', 'Preferences');
|
||||
|
||||
// Close options Menu
|
||||
cy.get('[data-cy="options-menu"]').click();
|
||||
});
|
||||
|
||||
it('checks if Preferences set in Study List Page will be consistent on Viewer Page', function() {
|
||||
// Go go hotkeys tab
|
||||
cy.get('@userPreferencesHotkeysTab')
|
||||
.click()
|
||||
.should('have.class', 'active');
|
||||
|
||||
// Set new hotkey for 'Rotate Right' function
|
||||
cy.setNewHotkeyShortcutOnUserPreferencesModal('Rotate Right', '{shift}Q');
|
||||
// Save new hotkey
|
||||
cy.get('@saveBtn')
|
||||
.scrollIntoView()
|
||||
.click();
|
||||
|
||||
// Open User Preferences modal again
|
||||
cy.openPreferences();
|
||||
|
||||
// Go to General tab
|
||||
cy.get('@userPreferencesGeneralTab').click();
|
||||
|
||||
// Select Spanish and Save
|
||||
cy.get('#language-select').select('Spanish');
|
||||
cy.get('@saveBtn')
|
||||
.scrollIntoView()
|
||||
.click();
|
||||
|
||||
// Go to Study Viewer page
|
||||
cy.openStudy('MISTER^MR');
|
||||
cy.waitDicomImage();
|
||||
cy.expectMinimumThumbnails(5);
|
||||
cy.initCommonElementsAliases();
|
||||
|
||||
// Check if application is in Spanish
|
||||
// Header should be translated to Spanish
|
||||
cy.get('.research-use')
|
||||
.scrollIntoView()
|
||||
.should('have.text', 'SOLO USO PARA INVESTIGACIÓN');
|
||||
|
||||
// Options menu should be translated
|
||||
cy.get('[data-cy="options-menu"]')
|
||||
.should('have.text', 'Opciones')
|
||||
.click();
|
||||
cy.get('[data-cy="dd-item-menu"]')
|
||||
.first()
|
||||
.should('contain.text', 'Acerca de');
|
||||
cy.get('[data-cy="dd-item-menu"]')
|
||||
.last()
|
||||
.should('contain.text', 'Preferencias');
|
||||
|
||||
// Check if new hotkey is working on viewport
|
||||
cy.get('body').type('{shift}Q', { release: false });
|
||||
cy.get('@viewportInfoMidTop').should('contains.text', 'R');
|
||||
});
|
||||
});
|
||||
|
||||
context('Study Viewer Page', function() {
|
||||
before(() => {
|
||||
cy.openStudy('MISTER^MR');
|
||||
cy.expectMinimumThumbnails(5);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
cy.initCommonElementsAliases();
|
||||
cy.resetViewport();
|
||||
|
||||
cy.resetUserHoktkeyPreferences();
|
||||
// Open User Preferences modal
|
||||
cy.openPreferences();
|
||||
});
|
||||
|
||||
it('checks displayed information on User Preferences modal', function() {
|
||||
cy.get('@preferencesModal').should('contain.text', 'User Preferences');
|
||||
cy.get('@userPreferencesHotkeysTab')
|
||||
.should('have.text', 'Hotkeys')
|
||||
.and('have.class', 'active');
|
||||
cy.get('@userPreferencesGeneralTab').should('have.text', 'General');
|
||||
cy.get('@restoreBtn')
|
||||
.scrollIntoView()
|
||||
.should('have.text', 'Reset to Defaults');
|
||||
cy.get('@cancelBtn').should('have.text', 'Cancel');
|
||||
cy.get('@saveBtn').should('have.text', 'Save');
|
||||
|
||||
// Visual comparison
|
||||
cy.screenshot(
|
||||
'User Preferences Modal - Hotkeys tab initial state in Study List page'
|
||||
);
|
||||
cy.percyCanvasSnapshot(
|
||||
'User Preferences Modal - Hotkeys tab initial state in Study List page'
|
||||
);
|
||||
cy.get('[data-cy="close-button"]').click(); //close User Preferences modal
|
||||
});
|
||||
|
||||
it('checks translation by selecting Spanish language', function() {
|
||||
cy.get('@userPreferencesGeneralTab')
|
||||
.click()
|
||||
.should('have.class', 'active');
|
||||
|
||||
// Language dropdown should be displayed
|
||||
cy.get('#language-select').should('be.visible');
|
||||
|
||||
// Visual comparison
|
||||
cy.screenshot(
|
||||
'User Preferences Modal - General tab initial state in Study List page'
|
||||
);
|
||||
cy.percyCanvasSnapshot(
|
||||
'User Preferences Modal - General tab initial state in Study List page'
|
||||
);
|
||||
// Select Spanish and Save
|
||||
cy.get('#language-select').select('Spanish');
|
||||
cy.get('@saveBtn')
|
||||
.scrollIntoView()
|
||||
.click();
|
||||
|
||||
// Header should be translated to Spanish
|
||||
cy.get('.research-use')
|
||||
.scrollIntoView()
|
||||
.should('have.text', 'SOLO USO PARA INVESTIGACIÓN');
|
||||
|
||||
// Options menu should be translated
|
||||
cy.get('[data-cy="options-menu"]')
|
||||
.should('have.text', 'Opciones')
|
||||
.click();
|
||||
|
||||
cy.get('[data-cy="dd-item-menu"]')
|
||||
.first()
|
||||
.should('contain.text', 'Acerca de');
|
||||
cy.get('[data-cy="dd-item-menu"]')
|
||||
.last()
|
||||
.should('contain.text', 'Preferencias');
|
||||
|
||||
// Close Options menu
|
||||
cy.get('[data-cy="options-menu"]').click();
|
||||
});
|
||||
|
||||
it('checks if user can cancel the language selection and application will be in English', function() {
|
||||
// Set language to English and save
|
||||
cy.setLanguage('English');
|
||||
|
||||
// Set language to Spanish and cancel
|
||||
cy.setLanguage('Spanish', false);
|
||||
|
||||
// Header should be kept in English
|
||||
cy.get('.research-use')
|
||||
.scrollIntoView()
|
||||
.should('have.text', 'INVESTIGATIONAL USE ONLY');
|
||||
|
||||
// Options menu should be translated
|
||||
cy.get('[data-cy="options-menu"]')
|
||||
.should('have.text', 'Options')
|
||||
.click();
|
||||
|
||||
cy.get('[data-cy="dd-item-menu"]')
|
||||
.first()
|
||||
.should('contain.text', 'About');
|
||||
cy.get('[data-cy="dd-item-menu"]')
|
||||
.last()
|
||||
.should('contain.text', 'Preferences');
|
||||
// Close Options menu
|
||||
cy.get('[data-cy="options-menu"]').click();
|
||||
});
|
||||
|
||||
it('checks if user can restore to default the language selection and application will be in English', function() {
|
||||
cy.get('@userPreferencesGeneralTab')
|
||||
.click()
|
||||
.should('have.class', 'active');
|
||||
|
||||
// Language dropdown should be displayed
|
||||
cy.get('#language-select').should('be.visible');
|
||||
|
||||
// Select Spanish and Save
|
||||
cy.get('#language-select').select('Spanish');
|
||||
cy.get('@saveBtn')
|
||||
.scrollIntoView()
|
||||
.click();
|
||||
|
||||
//Open Preferences again
|
||||
cy.get('[data-cy="options-menu"]')
|
||||
.scrollIntoView()
|
||||
.click();
|
||||
cy.get('[data-cy="dd-item-menu"]')
|
||||
.last()
|
||||
.click();
|
||||
|
||||
// Go to general tab
|
||||
cy.get('@userPreferencesGeneralTab').click();
|
||||
|
||||
cy.get('@restoreBtn')
|
||||
.scrollIntoView()
|
||||
.click();
|
||||
|
||||
cy.get('@saveBtn')
|
||||
.scrollIntoView()
|
||||
.click();
|
||||
|
||||
// Header should be in English
|
||||
cy.get('.research-use')
|
||||
.scrollIntoView()
|
||||
.should('have.text', 'INVESTIGATIONAL USE ONLY');
|
||||
|
||||
// Options menu should be in English
|
||||
cy.get('[data-cy="options-menu"]')
|
||||
.should('have.text', 'Options')
|
||||
.click();
|
||||
cy.get('[data-cy="dd-item-menu"]')
|
||||
.first()
|
||||
.should('contain.text', 'About');
|
||||
cy.get('[data-cy="dd-item-menu"]')
|
||||
.last()
|
||||
.should('contain.text', 'Preferences');
|
||||
|
||||
// Close Options menu
|
||||
cy.get('[data-cy="options-menu"]').click();
|
||||
});
|
||||
|
||||
it('checks new hotkeys for "Rotate Right" and "Rotate Left"', function() {
|
||||
// Go go hotkeys tab
|
||||
cy.get('@userPreferencesHotkeysTab')
|
||||
.click()
|
||||
.should('have.class', 'active');
|
||||
|
||||
// Set new hotkey for 'Rotate Right' function
|
||||
cy.setNewHotkeyShortcutOnUserPreferencesModal(
|
||||
'Rotate Right',
|
||||
'{shift}{rightarrow}'
|
||||
);
|
||||
// Set new hotkey for 'Rotate Left' function
|
||||
cy.setNewHotkeyShortcutOnUserPreferencesModal(
|
||||
'Rotate Left',
|
||||
'{shift}{leftarrow}'
|
||||
);
|
||||
|
||||
//Save new hotkeys
|
||||
cy.get('@saveBtn')
|
||||
.scrollIntoView()
|
||||
.click();
|
||||
|
||||
//Rotate Right with new Hotkey
|
||||
cy.get('body').type('{shift}{rightarrow}');
|
||||
cy.get('@viewportInfoMidTop').should('contains.text', 'R');
|
||||
|
||||
//Rotate Left with new Hotkey
|
||||
cy.get('body').type('{shift}{leftarrow}');
|
||||
cy.get('@viewportInfoMidTop').should('contains.text', 'A');
|
||||
});
|
||||
|
||||
it('checks new hotkeys for "Next" and "Previous" Image on Viewport', function() {
|
||||
// Go go hotkeys tab
|
||||
cy.get('@userPreferencesHotkeysTab')
|
||||
.click()
|
||||
.should('have.class', 'active');
|
||||
|
||||
// Set new hotkey for 'Next Image Viewport' function
|
||||
cy.setNewHotkeyShortcutOnUserPreferencesModal(
|
||||
'Next Image Viewport',
|
||||
'{shift}{rightarrow}'
|
||||
);
|
||||
|
||||
// Set new hotkey for 'Previous Image Viewport' function
|
||||
cy.setNewHotkeyShortcutOnUserPreferencesModal(
|
||||
'Previous Image Viewport',
|
||||
'{shift}{leftarrow}'
|
||||
);
|
||||
|
||||
// Save new hotkeys
|
||||
cy.get('@saveBtn')
|
||||
.scrollIntoView()
|
||||
.click();
|
||||
|
||||
// Set 3 viewports layout
|
||||
cy.setLayout(3, 1);
|
||||
cy.waitViewportImageLoading();
|
||||
|
||||
// Rotate Right and Invert colors on Viewport #1
|
||||
cy.get('body').type('RI');
|
||||
// Check that image was rotated
|
||||
cy.get('@viewportInfoMidTop').should('contains.text', 'R');
|
||||
|
||||
//Move to Next Viewport
|
||||
cy.get('body').type('{shift}{rightarrow}');
|
||||
// Rotate Left and Invert colors on Viewport #2
|
||||
cy.get('body').type('LI');
|
||||
// Get overlay information from viewport #2
|
||||
cy.get(
|
||||
':nth-child(2) > .viewport-wrapper > .viewport-element > .ViewportOrientationMarkers.noselect > .top-mid.orientation-marker'
|
||||
).as('viewport2InfoMidTop');
|
||||
// Check that image was rotated
|
||||
cy.get('@viewport2InfoMidTop').should('contains.text', 'P');
|
||||
|
||||
//Move to Previous Viewport
|
||||
cy.get('body').type('{shift}{leftarrow}');
|
||||
// Reset viewport #1 with spacebar hotkey
|
||||
cy.get('body').type(' ');
|
||||
cy.get('@viewportInfoMidTop').should('contains.text', 'A');
|
||||
|
||||
// Visual comparison
|
||||
cy.screenshot('Viewport Navigation - 2nd viewport inverted and rotated');
|
||||
cy.percyCanvasSnapshot(
|
||||
'Viewport Navigation - 2nd viewport inverted and rotated'
|
||||
);
|
||||
// Set 1 viewport layout
|
||||
cy.setLayout(1, 1);
|
||||
});
|
||||
|
||||
it('checks error message when duplicated hotkeys are inserted', function() {
|
||||
// Go go hotkeys tab
|
||||
cy.get('@userPreferencesHotkeysTab').click();
|
||||
|
||||
// Set duplicated hotkey for 'Rotate Right' function
|
||||
cy.setNewHotkeyShortcutOnUserPreferencesModal(
|
||||
'Rotate Right',
|
||||
'{rightarrow}'
|
||||
);
|
||||
|
||||
// Check error message
|
||||
cy.get('.HotKeysPreferences').within(() => {
|
||||
cy.contains('Rotate Right') // label we're looking for
|
||||
.parent()
|
||||
.find('.errorMessage')
|
||||
.as('errorMsg')
|
||||
.should(
|
||||
'have.text',
|
||||
'"Next Image Viewport" is already using the "right" shortcut.'
|
||||
);
|
||||
});
|
||||
//Cancel hotkeys
|
||||
cy.get('@cancelBtn')
|
||||
.scrollIntoView()
|
||||
.click();
|
||||
});
|
||||
|
||||
it('checks error message when invalid hotkey is inserted', function() {
|
||||
// Go go hotkeys tab
|
||||
cy.get('@userPreferencesHotkeysTab').click();
|
||||
|
||||
// Set invalid hotkey for 'Rotate Right' function
|
||||
cy.setNewHotkeyShortcutOnUserPreferencesModal('Rotate Right', '{ctrl}Z');
|
||||
|
||||
// Check error message
|
||||
cy.get('.HotKeysPreferences').within(() => {
|
||||
cy.contains('Rotate Right') // label we're looking for
|
||||
.parent()
|
||||
.find('.errorMessage')
|
||||
.as('errorMsg')
|
||||
.should('have.text', '"ctrl+z" shortcut combination is not allowed');
|
||||
});
|
||||
|
||||
//Cancel hotkeys
|
||||
cy.get('@cancelBtn')
|
||||
.scrollIntoView()
|
||||
.click();
|
||||
});
|
||||
|
||||
it('checks error message when only modifier keys are inserted', function() {
|
||||
// Go go hotkeys tab
|
||||
cy.get('@userPreferencesHotkeysTab').click();
|
||||
|
||||
// Set invalid modifier key: ctrl
|
||||
cy.setNewHotkeyShortcutOnUserPreferencesModal('Zoom Out', '{ctrl}');
|
||||
// Check error message
|
||||
cy.get('.HotKeysPreferences').within(() => {
|
||||
cy.contains('Zoom Out') // label we're looking for
|
||||
.parent()
|
||||
.find('.errorMessage')
|
||||
.as('errorMsg')
|
||||
.should(
|
||||
'have.text',
|
||||
"It's not possible to define only modifier keys (ctrl, alt and shift) as a shortcut"
|
||||
);
|
||||
});
|
||||
|
||||
// Set invalid modifier key: shift
|
||||
cy.setNewHotkeyShortcutOnUserPreferencesModal('Zoom Out', '{shift}');
|
||||
// Check error message
|
||||
cy.get('@errorMsg').should(
|
||||
'have.text',
|
||||
"It's not possible to define only modifier keys (ctrl, alt and shift) as a shortcut"
|
||||
);
|
||||
|
||||
// Set invalid modifier key: alt
|
||||
cy.setNewHotkeyShortcutOnUserPreferencesModal('Zoom Out', '{alt}');
|
||||
// Check error message
|
||||
cy.get('@errorMsg').should(
|
||||
'have.text',
|
||||
"It's not possible to define only modifier keys (ctrl, alt and shift) as a shortcut"
|
||||
);
|
||||
|
||||
//Cancel hotkeys
|
||||
cy.get('@cancelBtn')
|
||||
.scrollIntoView()
|
||||
.click();
|
||||
});
|
||||
|
||||
it('checks if user can cancel changes made on User Preferences Hotkeys tab', function() {
|
||||
// Go go hotkeys tab
|
||||
cy.get('@userPreferencesHotkeysTab').click();
|
||||
|
||||
// Set new hotkey for 'Rotate Right' function
|
||||
cy.setNewHotkeyShortcutOnUserPreferencesModal(
|
||||
'Rotate Right',
|
||||
'{ctrl}{shift}S'
|
||||
);
|
||||
|
||||
//Cancel hotkeys
|
||||
cy.get('@cancelBtn')
|
||||
.scrollIntoView()
|
||||
.click();
|
||||
|
||||
// Open User Preferences modal again
|
||||
cy.openPreferences();
|
||||
|
||||
//Check that hotkey for 'Rotate Right' function was not changed
|
||||
cy.get('.HotKeysPreferences').within(() => {
|
||||
cy.contains('Rotate Right') // label we're looking for
|
||||
.parent()
|
||||
.find('input')
|
||||
.should('have.value', 'r');
|
||||
});
|
||||
cy.get('[data-cy="close-button"]').click();
|
||||
});
|
||||
|
||||
it('checks if user can reset to default values on User Preferences Hotkeys tab', function() {
|
||||
// Go go hotkeys tab
|
||||
cy.get('@userPreferencesHotkeysTab').click();
|
||||
|
||||
// Set new hotkey for 'Rotate Right' function
|
||||
cy.setNewHotkeyShortcutOnUserPreferencesModal(
|
||||
'Rotate Right',
|
||||
'{ctrl}{shift}S'
|
||||
);
|
||||
|
||||
//Save hotkeys
|
||||
cy.get('@saveBtn')
|
||||
.scrollIntoView()
|
||||
.click();
|
||||
|
||||
// Open User Preferences modal again
|
||||
cy.openPreferences();
|
||||
|
||||
//Restore Default hotkeys
|
||||
cy.get('@restoreBtn')
|
||||
.scrollIntoView()
|
||||
.click();
|
||||
|
||||
// Open User Preferences modal again
|
||||
cy.openPreferences();
|
||||
|
||||
//Check that hotkey for 'Rotate Right' function was not changed
|
||||
cy.get('.HotKeysPreferences').within(() => {
|
||||
cy.contains('Rotate Right') // label we're looking for
|
||||
.parent()
|
||||
.find('input')
|
||||
.should('have.value', 'r');
|
||||
});
|
||||
cy.get('[data-cy="close-button"]').click();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -81,6 +81,7 @@ describe('OHIF VTK Extension', () => {
|
||||
);
|
||||
});
|
||||
|
||||
/* TODO: Non-deterministic behavior (const expectedText = 'W: 350 L: -1044';)
|
||||
it('checks WWWC tool', () => {
|
||||
cy.get('@wwwcBtn').click();
|
||||
|
||||
@@ -92,7 +93,7 @@ describe('OHIF VTK Extension', () => {
|
||||
.trigger('mousemove', 'top', { which: 1 })
|
||||
.trigger('mouseup', { which: 1 })
|
||||
.then(() => {
|
||||
const expectedText = 'W: 350 L: -1044';
|
||||
const expectedText = 'W: 350 L: 40';
|
||||
cy.get('.ViewportOverlay > div.bottom-right.overlay-element').should(
|
||||
'contains.text',
|
||||
expectedText
|
||||
@@ -102,7 +103,7 @@ describe('OHIF VTK Extension', () => {
|
||||
// Visual comparison
|
||||
cy.screenshot('VTK WWWC tool - Canvas should be bright');
|
||||
cy.percyCanvasSnapshot('VTK WWWC tool - Canvas should be bright');
|
||||
});
|
||||
}); */
|
||||
|
||||
it('checks Rotate tool', () => {
|
||||
cy.get('@rotateBtn').click();
|
||||
|
||||
@@ -77,3 +77,17 @@ export function initStudyListAliasesOnTablet() {
|
||||
cy.get('#end-date').as('studyListEndDate');
|
||||
cy.get('[data-cy="study-list-results"] > tr').as('searchResult');
|
||||
}
|
||||
|
||||
//Creating aliases for User Preferences modal
|
||||
export function initPreferencesModalAliases() {
|
||||
cy.get('.OHIFModal').as('preferencesModal');
|
||||
cy.get('.nav-link')
|
||||
.first()
|
||||
.as('userPreferencesHotkeysTab');
|
||||
cy.get('.nav-link')
|
||||
.last()
|
||||
.as('userPreferencesGeneralTab');
|
||||
cy.get('[data-cy="reset-default-btn"]').as('restoreBtn');
|
||||
cy.get('[data-cy="cancel-btn"]').as('cancelBtn');
|
||||
cy.get('[data-cy="save-btn"]').as('saveBtn');
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
initVTKToolsAliases,
|
||||
initStudyListAliasesOnDesktop,
|
||||
initStudyListAliasesOnTablet,
|
||||
initPreferencesModalAliases,
|
||||
} from './aliases.js';
|
||||
|
||||
// ***********************************************
|
||||
@@ -102,6 +103,13 @@ Cypress.Commands.add('waitVTKReformatting', () => {
|
||||
});
|
||||
});
|
||||
|
||||
Cypress.Commands.add('waitViewportImageLoading', () => {
|
||||
// Wait for finish loading
|
||||
cy.get('[data-cy="viewprt-grid"]', { timeout: 20000 }).should($grid => {
|
||||
expect($grid).not.to.contain.text('Load');
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Command to perform a drag and drop action. Before using this command, we must get the element that should be dragged first.
|
||||
* Example of usage: cy.get(element-to-be-dragged).drag(dropzone-element)
|
||||
@@ -200,10 +208,10 @@ Cypress.Commands.add('waitDicomImage', (timeout = 20000) => {
|
||||
|
||||
//Command to reset and clear all the changes made to the viewport
|
||||
Cypress.Commands.add('resetViewport', () => {
|
||||
cy.initCornerstoneToolsAliases();
|
||||
|
||||
//Click on More button
|
||||
cy.get('@moreBtn').click();
|
||||
cy.get('.expandableToolMenu')
|
||||
.as('moreBtn')
|
||||
.click();
|
||||
//Verify if overlay is displayed
|
||||
cy.get('body').then(body => {
|
||||
if (body.find('.tooltip-toolbar-overlay').length == 0) {
|
||||
@@ -215,7 +223,9 @@ Cypress.Commands.add('resetViewport', () => {
|
||||
.as('clearBtn')
|
||||
.click();
|
||||
//Click on Reset button
|
||||
cy.get('@resetBtn').click();
|
||||
cy.get('.ToolbarRow > :nth-child(9)')
|
||||
.as('resetBtn')
|
||||
.click();
|
||||
});
|
||||
|
||||
Cypress.Commands.add('imageZoomIn', () => {
|
||||
@@ -359,9 +369,7 @@ Cypress.Commands.add('percyCanvasSnapshot', (name, options = {}) => {
|
||||
});
|
||||
|
||||
Cypress.Commands.add('setLayout', (columns = 1, rows = 1) => {
|
||||
cy.get('.toolbar-button-label')
|
||||
.contains('Layout')
|
||||
.click();
|
||||
cy.get('.btn-group > .toolbar-button').click();
|
||||
|
||||
cy.get('.layoutChooser')
|
||||
.find('tr')
|
||||
@@ -410,3 +418,74 @@ function canvasToImage(selectorOrEl) {
|
||||
canvas.parentElement.appendChild(image);
|
||||
canvas.style = 'display: none';
|
||||
}
|
||||
|
||||
//Initialize aliases for User Preferences modal
|
||||
Cypress.Commands.add('initPreferencesModalAliases', () => {
|
||||
initPreferencesModalAliases();
|
||||
});
|
||||
|
||||
Cypress.Commands.add('openPreferences', () => {
|
||||
cy.log('Open User Preferences Modal');
|
||||
// Open User Preferences modal
|
||||
cy.get('body').then(body => {
|
||||
if (body.find('.OHIFModal').length === 0) {
|
||||
cy.get('[data-cy="options-menu"]')
|
||||
.scrollIntoView()
|
||||
.click()
|
||||
.then(() => {
|
||||
cy.get('[data-cy="dd-item-menu"]')
|
||||
.last()
|
||||
.click()
|
||||
.wait(200);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Cypress.Commands.add('resetUserHoktkeyPreferences', () => {
|
||||
// Open User Preferences modal
|
||||
cy.openPreferences();
|
||||
|
||||
cy.initPreferencesModalAliases();
|
||||
|
||||
cy.log('Reset to Default Preferences');
|
||||
cy.get('@restoreBtn').click();
|
||||
cy.get('@saveBtn').click();
|
||||
});
|
||||
|
||||
Cypress.Commands.add(
|
||||
'setNewHotkeyShortcutOnUserPreferencesModal',
|
||||
(function_label, shortcut) => {
|
||||
// Within scopes all `.get` and `.contains` to within the matched elements
|
||||
// dom instead of checking from document
|
||||
cy.get('.HotKeysPreferences')
|
||||
.within(() => {
|
||||
cy.contains(function_label) // label we're looking for
|
||||
.parent()
|
||||
.find('input') // closest input to that label
|
||||
.type(shortcut, { force: true }); // Set new shortcut for that function
|
||||
})
|
||||
.blur();
|
||||
}
|
||||
);
|
||||
|
||||
Cypress.Commands.add('setLanguage', (language, save = true) => {
|
||||
cy.openPreferences();
|
||||
|
||||
cy.get('@userPreferencesGeneralTab')
|
||||
.click()
|
||||
.should('have.class', 'active');
|
||||
|
||||
// Language dropdown should be displayed
|
||||
cy.get('#language-select').should('be.visible');
|
||||
|
||||
// Select Language and Save/Cancel
|
||||
cy.get('#language-select')
|
||||
.select(language)
|
||||
.then(() => {
|
||||
const toClick = save ? '@saveBtn' : '@cancelBtn';
|
||||
cy.get(toClick)
|
||||
.scrollIntoView()
|
||||
.click();
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/viewer",
|
||||
"version": "2.8.4",
|
||||
"version": "2.11.2",
|
||||
"description": "OHIF Viewer",
|
||||
"author": "OHIF Contributors",
|
||||
"license": "MIT",
|
||||
@@ -45,20 +45,20 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.5.5",
|
||||
"@ohif/core": "^1.10.0",
|
||||
"@ohif/core": "^1.13.1",
|
||||
"@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.8",
|
||||
"@ohif/i18n": "^0.52.0",
|
||||
"@ohif/ui": "^0.62.3",
|
||||
"@ohif/extension-vtk": "^0.54.1",
|
||||
"@ohif/i18n": "^0.52.2",
|
||||
"@ohif/ui": "^0.65.1",
|
||||
"@tanem/react-nprogress": "^1.1.25",
|
||||
"classnames": "^2.2.6",
|
||||
"core-js": "^3.2.1",
|
||||
"cornerstone-core": "^2.2.8",
|
||||
"cornerstone-math": "^0.1.8",
|
||||
"cornerstone-tools": "^4.0.9",
|
||||
"cornerstone-tools": "^4.8.0",
|
||||
"cornerstone-wado-image-loader": "^3.0.0",
|
||||
"dcmjs": "^0.6.1",
|
||||
"dicom-parser": "^1.8.3",
|
||||
|
||||
@@ -3,6 +3,7 @@ window.config = {
|
||||
routerBasename: '/',
|
||||
extensions: [],
|
||||
showStudyList: true,
|
||||
filterQueryParam: false,
|
||||
servers: {
|
||||
dicomWeb: [
|
||||
{
|
||||
|
||||
+74
-32
@@ -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,50 @@ import {
|
||||
HotkeysManager,
|
||||
createUINotificationService,
|
||||
createUIModalService,
|
||||
createUIDialogService,
|
||||
utils,
|
||||
redux as reduxOHIF
|
||||
} 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';
|
||||
const { setUserPreferences } = reduxOHIF.actions;
|
||||
|
||||
/** 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 +72,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 +89,7 @@ class App extends Component {
|
||||
id: PropTypes.string.isRequired,
|
||||
})
|
||||
),
|
||||
hotkeys: PropTypes.array,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
@@ -91,7 +109,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 +132,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 +158,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>
|
||||
@@ -201,9 +223,29 @@ function _initExtensions(extensions, hotkeys) {
|
||||
const mergedExtensions = defaultExtensions.concat(extensions);
|
||||
extensionManager.registerExtensions(mergedExtensions);
|
||||
|
||||
const { hotkeyDefinitions = {} } = store.getState().preferences || {};
|
||||
let updateStore = false;
|
||||
let hotkeysToUse = hotkeyDefinitions;
|
||||
|
||||
// Must run after extension commands are registered
|
||||
if (hotkeys) {
|
||||
hotkeysManager.setHotkeys(hotkeys, true);
|
||||
// if there is no hotkeys from localStorate set up from config
|
||||
if (!Object.keys(hotkeyDefinitions).length) {
|
||||
hotkeysToUse = hotkeys;
|
||||
updateStore = true;
|
||||
}
|
||||
|
||||
if (hotkeysToUse) {
|
||||
hotkeysManager.setHotkeys(hotkeysToUse);
|
||||
// set default based on app config
|
||||
hotkeysManager.setDefaultHotKeys(hotkeys);
|
||||
|
||||
if (updateStore) {
|
||||
const { hotkeyDefinitions } = hotkeysManager;
|
||||
const windowLevelData = {};
|
||||
store.dispatch(
|
||||
setUserPreferences({ windowLevelData, hotkeyDefinitions })
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
getResetLabellingAndContextMenu,
|
||||
} from './labelingFlowCallbacks.js';
|
||||
import throttle from 'lodash.throttle';
|
||||
import { SimpleDialog } from '@ohif/ui';
|
||||
|
||||
// TODO: This only works because we have a hard dependency on this extension
|
||||
// We need to decouple and make stuff like this possible w/o bundling this at
|
||||
@@ -35,6 +36,28 @@ const MEASUREMENT_ACTION_MAP = {
|
||||
* @param {*} configuration
|
||||
*/
|
||||
export default function init({ servicesManager, configuration = {} }) {
|
||||
const { UIDialogService } = servicesManager.services;
|
||||
const callInputDialog = (data, event, callback) => {
|
||||
let dialogId = UIDialogService.create({
|
||||
content: SimpleDialog.InputDialog,
|
||||
defaultPosition: {
|
||||
x: (event && event.currentPoints.canvas.x) || 0,
|
||||
y: (event && event.currentPoints.canvas.y) || 0,
|
||||
},
|
||||
showOverlay: true,
|
||||
contentProps: {
|
||||
title: 'Enter your annotation',
|
||||
label: 'New label',
|
||||
defaultValue: data ? data.text : '',
|
||||
onClose: () => UIDialogService.dismiss({ id: dialogId }),
|
||||
onSubmit: value => {
|
||||
callback(value);
|
||||
UIDialogService.dismiss({ id: dialogId });
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// If these tools were already added by a different extension, we want to replace
|
||||
// them with the same tools that have an alternative configuration. By passing in
|
||||
// our custom `getMeasurementLocationCallback`, we can...
|
||||
@@ -90,6 +113,10 @@ export default function init({ servicesManager, configuration = {} }) {
|
||||
csTools.addTool(csTools.ArrowAnnotateTool, {
|
||||
configuration: {
|
||||
getMeasurementLocationCallback: toolLabellingFlowCallback,
|
||||
getTextCallback: (callback, eventDetails) =>
|
||||
callInputDialog(null, eventDetails, callback),
|
||||
changeTextCallback: (data, eventDetails, callback) =>
|
||||
callInputDialog(data, eventDetails, callback),
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
Loaded 100 of 111 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user