Compare commits
50
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e87ec7b7f5 | ||
|
|
d98889f1eb | ||
|
|
817d43ee62 | ||
|
|
f396b30166 | ||
|
|
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 | ||
|
|
57730483e9 | ||
|
|
731b64e321 | ||
|
|
e97a09b0f4 | ||
|
|
f63d8a73d8 | ||
|
|
add3416e11 | ||
|
|
136969bf18 | ||
|
|
e3e039ad47 |
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,44 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.7.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@1.7.1...@ohif/extension-cornerstone@1.7.2) (2019-12-02)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.7.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@1.7.0...@ohif/extension-cornerstone@1.7.1) (2019-12-02)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [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.2",
|
||||
"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",
|
||||
|
||||
@@ -2,6 +2,7 @@ import CornerstoneViewport from 'react-cornerstone-viewport';
|
||||
import OHIF from '@ohif/core';
|
||||
import { connect } from 'react-redux';
|
||||
import throttle from 'lodash.throttle';
|
||||
import { setEnabledElement } from './state';
|
||||
|
||||
const { setViewportActive, setViewportSpecificData } = OHIF.redux.actions;
|
||||
const {
|
||||
@@ -80,11 +81,11 @@ const mapDispatchToProps = (dispatch, ownProps) => {
|
||||
*/
|
||||
onElementEnabled: event => {
|
||||
const enabledElement = event.detail.element;
|
||||
setEnabledElement(viewportIndex, enabledElement);
|
||||
dispatch(
|
||||
setViewportSpecificData(viewportIndex, {
|
||||
// TODO: Hack to make sure our plugin info is available from the outset
|
||||
plugin: 'cornerstone',
|
||||
dom: enabledElement,
|
||||
})
|
||||
);
|
||||
},
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
import React from 'react';
|
||||
import cornerstone from 'cornerstone-core';
|
||||
import cornerstoneTools from 'cornerstone-tools';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { ViewportDownloadForm } from '@ohif/ui';
|
||||
import { utils } from '@ohif/core';
|
||||
|
||||
import { getEnabledElement } from './state';
|
||||
|
||||
const MINIMUM_SIZE = 100;
|
||||
const DEFAULT_SIZE = 512;
|
||||
const MAX_TEXTURE_SIZE = 10000;
|
||||
|
||||
const CornerstoneViewportDownloadForm = ({ onClose, activeViewportIndex }) => {
|
||||
const activeEnabledElement = getEnabledElement(activeViewportIndex);
|
||||
|
||||
const enableViewport = viewportElement => {
|
||||
if (viewportElement) {
|
||||
cornerstone.enable(viewportElement);
|
||||
}
|
||||
};
|
||||
|
||||
const disableViewport = viewportElement => {
|
||||
if (viewportElement) {
|
||||
cornerstone.disable(viewportElement);
|
||||
}
|
||||
};
|
||||
|
||||
const updateViewportPreview = (viewportElement, downloadCanvas, fileType) =>
|
||||
new Promise(resolve => {
|
||||
cornerstone.fitToWindow(viewportElement);
|
||||
|
||||
viewportElement.addEventListener(
|
||||
'cornerstoneimagerendered',
|
||||
function updateViewport(event) {
|
||||
const enabledElement = cornerstone.getEnabledElement(event.target)
|
||||
.element;
|
||||
const type = 'image/' + fileType;
|
||||
const dataUrl = downloadCanvas.toDataURL(type, 1);
|
||||
|
||||
let newWidth = enabledElement.offsetHeight;
|
||||
let newHeight = enabledElement.offsetWidth;
|
||||
|
||||
if (newWidth > DEFAULT_SIZE || newHeight > DEFAULT_SIZE) {
|
||||
const multiplier = DEFAULT_SIZE / Math.max(newWidth, newHeight);
|
||||
newHeight *= multiplier;
|
||||
newWidth *= multiplier;
|
||||
}
|
||||
|
||||
resolve({ dataUrl, width: newWidth, height: newHeight });
|
||||
|
||||
viewportElement.removeEventListener(
|
||||
'cornerstoneimagerendered',
|
||||
updateViewport
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
const loadImage = (activeViewport, viewportElement, width, height) =>
|
||||
new Promise(resolve => {
|
||||
if (activeViewport && viewportElement) {
|
||||
const enabledElement = cornerstone.getEnabledElement(activeViewport);
|
||||
const viewport = Object.assign({}, enabledElement.viewport);
|
||||
delete viewport.scale;
|
||||
viewport.translation = {
|
||||
x: 0,
|
||||
y: 0,
|
||||
};
|
||||
|
||||
cornerstone.loadImage(enabledElement.image.imageId).then(image => {
|
||||
cornerstone.displayImage(viewportElement, image);
|
||||
cornerstone.setViewport(viewportElement, viewport);
|
||||
cornerstone.resize(viewportElement, true);
|
||||
|
||||
const newWidth = Math.min(width || image.width, MAX_TEXTURE_SIZE);
|
||||
const newHeight = Math.min(height || image.height, MAX_TEXTURE_SIZE);
|
||||
|
||||
resolve({ image, width: newWidth, height: newHeight });
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const toggleAnnotations = (toggle, viewportElement) => {
|
||||
cornerstoneTools.store.state.tools.forEach(({ name }) => {
|
||||
if (toggle) {
|
||||
cornerstoneTools.setToolEnabledForElement(viewportElement, name);
|
||||
} else {
|
||||
cornerstoneTools.setToolDisabledForElement(viewportElement, name);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const downloadBlob = (
|
||||
filename,
|
||||
fileType,
|
||||
viewportElement,
|
||||
downloadCanvas
|
||||
) => {
|
||||
const file = `${filename}.${fileType}`;
|
||||
const mimetype = `image/${fileType}`;
|
||||
|
||||
/* Handles JPEG images for IE11 */
|
||||
if (downloadCanvas.msToBlob && fileType === 'jpeg') {
|
||||
const image = downloadCanvas.toDataURL(mimetype, 1);
|
||||
const blob = utils.b64toBlob(
|
||||
image.replace('data:image/jpeg;base64,', ''),
|
||||
mimetype
|
||||
);
|
||||
return window.navigator.msSaveBlob(blob, file);
|
||||
}
|
||||
|
||||
viewportElement.querySelector('canvas').toBlob(blob => {
|
||||
const URLObj = window.URL || window.webkitURL;
|
||||
const a = document.createElement('a');
|
||||
a.href = URLObj.createObjectURL(blob);
|
||||
a.download = file;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<ViewportDownloadForm
|
||||
onClose={onClose}
|
||||
minimumSize={MINIMUM_SIZE}
|
||||
maximumSize={MAX_TEXTURE_SIZE}
|
||||
defaultSize={DEFAULT_SIZE}
|
||||
canvasClass={'cornerstone-canvas'}
|
||||
activeViewport={activeEnabledElement}
|
||||
enableViewport={enableViewport}
|
||||
disableViewport={disableViewport}
|
||||
updateViewportPreview={updateViewportPreview}
|
||||
loadImage={loadImage}
|
||||
toggleAnnotations={toggleAnnotations}
|
||||
downloadBlob={downloadBlob}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
CornerstoneViewportDownloadForm.propTypes = {
|
||||
onClose: PropTypes.func,
|
||||
activeViewportIndex: PropTypes.number.isRequired,
|
||||
};
|
||||
|
||||
export default CornerstoneViewportDownloadForm;
|
||||
@@ -1,261 +1,260 @@
|
||||
import cornerstone from 'cornerstone-core';
|
||||
import cornerstoneTools from 'cornerstone-tools';
|
||||
import OHIF from '@ohif/core';
|
||||
|
||||
import { getEnabledElement } from './state';
|
||||
import CornerstoneViewportDownloadForm from './CornerstoneViewportDownloadForm';
|
||||
const scroll = cornerstoneTools.import('util/scroll');
|
||||
|
||||
const actions = {
|
||||
rotateViewport: ({ viewports, rotation }) => {
|
||||
const enabledElement = _getActiveViewportEnabledElement(
|
||||
viewports.viewportSpecificData,
|
||||
viewports.activeViewportIndex
|
||||
);
|
||||
const commandsModule = ({ servicesManager }) => {
|
||||
const actions = {
|
||||
rotateViewport: ({ viewports, rotation }) => {
|
||||
const enabledElement = getEnabledElement(viewports.activeViewportIndex);
|
||||
|
||||
if (enabledElement) {
|
||||
let viewport = cornerstone.getViewport(enabledElement);
|
||||
viewport.rotation += rotation;
|
||||
cornerstone.setViewport(enabledElement, viewport);
|
||||
}
|
||||
},
|
||||
flipViewportHorizontal: ({ viewports }) => {
|
||||
const enabledElement = _getActiveViewportEnabledElement(
|
||||
viewports.viewportSpecificData,
|
||||
viewports.activeViewportIndex
|
||||
);
|
||||
|
||||
if (enabledElement) {
|
||||
let viewport = cornerstone.getViewport(enabledElement);
|
||||
viewport.hflip = !viewport.hflip;
|
||||
cornerstone.setViewport(enabledElement, viewport);
|
||||
}
|
||||
},
|
||||
flipViewportVertical: ({ viewports }) => {
|
||||
const enabledElement = _getActiveViewportEnabledElement(
|
||||
viewports.viewportSpecificData,
|
||||
viewports.activeViewportIndex
|
||||
);
|
||||
|
||||
if (enabledElement) {
|
||||
let viewport = cornerstone.getViewport(enabledElement);
|
||||
viewport.vflip = !viewport.vflip;
|
||||
cornerstone.setViewport(enabledElement, viewport);
|
||||
}
|
||||
},
|
||||
scaleViewport: ({ viewports, direction }) => {
|
||||
const enabledElement = _getActiveViewportEnabledElement(
|
||||
viewports.viewportSpecificData,
|
||||
viewports.activeViewportIndex
|
||||
);
|
||||
const step = direction * 0.15;
|
||||
|
||||
if (enabledElement) {
|
||||
if (step) {
|
||||
if (enabledElement) {
|
||||
let viewport = cornerstone.getViewport(enabledElement);
|
||||
viewport.scale += step;
|
||||
viewport.rotation += rotation;
|
||||
cornerstone.setViewport(enabledElement, viewport);
|
||||
} else {
|
||||
cornerstone.fitToWindow(enabledElement);
|
||||
}
|
||||
}
|
||||
},
|
||||
resetViewport: ({ viewports }) => {
|
||||
const enabledElement = _getActiveViewportEnabledElement(
|
||||
viewports.viewportSpecificData,
|
||||
viewports.activeViewportIndex
|
||||
);
|
||||
},
|
||||
flipViewportHorizontal: ({ viewports }) => {
|
||||
const enabledElement = getEnabledElement(viewports.activeViewportIndex);
|
||||
|
||||
if (enabledElement) {
|
||||
cornerstone.reset(enabledElement);
|
||||
}
|
||||
},
|
||||
invertViewport: ({ viewports }) => {
|
||||
const enabledElement = _getActiveViewportEnabledElement(
|
||||
viewports.viewportSpecificData,
|
||||
viewports.activeViewportIndex
|
||||
);
|
||||
if (enabledElement) {
|
||||
let viewport = cornerstone.getViewport(enabledElement);
|
||||
viewport.hflip = !viewport.hflip;
|
||||
cornerstone.setViewport(enabledElement, viewport);
|
||||
}
|
||||
},
|
||||
flipViewportVertical: ({ viewports }) => {
|
||||
const enabledElement = getEnabledElement(viewports.activeViewportIndex);
|
||||
|
||||
if (enabledElement) {
|
||||
let viewport = cornerstone.getViewport(enabledElement);
|
||||
viewport.invert = !viewport.invert;
|
||||
cornerstone.setViewport(enabledElement, viewport);
|
||||
}
|
||||
},
|
||||
// TODO: this is receiving `evt` from `ToolbarRow`. We could use it to have
|
||||
// better mouseButtonMask sets.
|
||||
setToolActive: ({ toolName }) => {
|
||||
if (!toolName) {
|
||||
console.warn('No toolname provided to setToolActive command');
|
||||
}
|
||||
cornerstoneTools.setToolActive(toolName, { mouseButtonMask: 1 });
|
||||
},
|
||||
updateViewportDisplaySet: ({ direction }) => {
|
||||
// TODO
|
||||
console.warn('updateDisplaySet: ', direction);
|
||||
},
|
||||
clearAnnotations: ({ viewports }) => {
|
||||
const element = _getActiveViewportEnabledElement(
|
||||
viewports.viewportSpecificData,
|
||||
viewports.activeViewportIndex
|
||||
);
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
if (enabledElement) {
|
||||
let viewport = cornerstone.getViewport(enabledElement);
|
||||
viewport.vflip = !viewport.vflip;
|
||||
cornerstone.setViewport(enabledElement, viewport);
|
||||
}
|
||||
},
|
||||
scaleViewport: ({ direction, viewports }) => {
|
||||
const enabledElement = getEnabledElement(viewports.activeViewportIndex);
|
||||
const step = direction * 0.15;
|
||||
|
||||
const enabledElement = cornerstone.getEnabledElement(element);
|
||||
if (!enabledElement || !enabledElement.image) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {
|
||||
toolState,
|
||||
} = cornerstoneTools.globalImageIdSpecificToolStateManager;
|
||||
if (
|
||||
!toolState ||
|
||||
toolState.hasOwnProperty(enabledElement.image.imageId) === false
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const imageIdToolState = toolState[enabledElement.image.imageId];
|
||||
|
||||
const measurementsToRemove = [];
|
||||
|
||||
Object.keys(imageIdToolState).forEach(toolType => {
|
||||
const { data } = imageIdToolState[toolType];
|
||||
|
||||
data.forEach(measurementData => {
|
||||
const { _id, lesionNamingNumber, measurementNumber } = measurementData;
|
||||
if (!_id) {
|
||||
return;
|
||||
if (enabledElement) {
|
||||
if (step) {
|
||||
let viewport = cornerstone.getViewport(enabledElement);
|
||||
viewport.scale += step;
|
||||
cornerstone.setViewport(enabledElement, viewport);
|
||||
} else {
|
||||
cornerstone.fitToWindow(enabledElement);
|
||||
}
|
||||
}
|
||||
},
|
||||
resetViewport: ({ viewports }) => {
|
||||
const enabledElement = getEnabledElement(viewports.activeViewportIndex);
|
||||
|
||||
measurementsToRemove.push({
|
||||
toolType,
|
||||
_id,
|
||||
lesionNamingNumber,
|
||||
measurementNumber,
|
||||
if (enabledElement) {
|
||||
cornerstone.reset(enabledElement);
|
||||
}
|
||||
},
|
||||
invertViewport: ({ viewports }) => {
|
||||
const enabledElement = getEnabledElement(viewports.activeViewportIndex);
|
||||
|
||||
if (enabledElement) {
|
||||
let viewport = cornerstone.getViewport(enabledElement);
|
||||
viewport.invert = !viewport.invert;
|
||||
cornerstone.setViewport(enabledElement, viewport);
|
||||
}
|
||||
},
|
||||
// TODO: this is receiving `evt` from `ToolbarRow`. We could use it to have
|
||||
// better mouseButtonMask sets.
|
||||
setToolActive: ({ toolName }) => {
|
||||
if (!toolName) {
|
||||
console.warn('No toolname provided to setToolActive command');
|
||||
}
|
||||
cornerstoneTools.setToolActive(toolName, { mouseButtonMask: 1 });
|
||||
},
|
||||
updateViewportDisplaySet: ({ direction }) => {
|
||||
// TODO
|
||||
console.warn('updateDisplaySet: ', direction);
|
||||
},
|
||||
clearAnnotations: ({ viewports }) => {
|
||||
const element = getEnabledElement(viewports.activeViewportIndex);
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
|
||||
const enabledElement = cornerstone.getEnabledElement(element);
|
||||
if (!enabledElement || !enabledElement.image) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {
|
||||
toolState,
|
||||
} = cornerstoneTools.globalImageIdSpecificToolStateManager;
|
||||
if (
|
||||
!toolState ||
|
||||
toolState.hasOwnProperty(enabledElement.image.imageId) === false
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const imageIdToolState = toolState[enabledElement.image.imageId];
|
||||
|
||||
const measurementsToRemove = [];
|
||||
|
||||
Object.keys(imageIdToolState).forEach(toolType => {
|
||||
const { data } = imageIdToolState[toolType];
|
||||
|
||||
data.forEach(measurementData => {
|
||||
const {
|
||||
_id,
|
||||
lesionNamingNumber,
|
||||
measurementNumber,
|
||||
} = measurementData;
|
||||
if (!_id) {
|
||||
return;
|
||||
}
|
||||
|
||||
measurementsToRemove.push({
|
||||
toolType,
|
||||
_id,
|
||||
lesionNamingNumber,
|
||||
measurementNumber,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
measurementsToRemove.forEach(measurementData => {
|
||||
OHIF.measurements.MeasurementHandlers.onRemoved({
|
||||
detail: {
|
||||
toolType: measurementData.toolType,
|
||||
measurementData,
|
||||
measurementsToRemove.forEach(measurementData => {
|
||||
OHIF.measurements.MeasurementHandlers.onRemoved({
|
||||
detail: {
|
||||
toolType: measurementData.toolType,
|
||||
measurementData,
|
||||
},
|
||||
});
|
||||
});
|
||||
},
|
||||
nextImage: ({ viewports }) => {
|
||||
const enabledElement = getEnabledElement(viewports.activeViewportIndex);
|
||||
scroll(enabledElement, 1);
|
||||
},
|
||||
previousImage: ({ viewports }) => {
|
||||
const enabledElement = getEnabledElement(viewports.activeViewportIndex);
|
||||
scroll(enabledElement, -1);
|
||||
},
|
||||
getActiveViewportEnabledElement: ({ viewports }) => {
|
||||
const enabledElement = getEnabledElement(viewports.activeViewportIndex);
|
||||
return enabledElement;
|
||||
},
|
||||
showDownloadViewportModal: ({ title, viewports }) => {
|
||||
const activeViewportIndex = viewports.activeViewportIndex;
|
||||
const { UIModalService } = servicesManager.services;
|
||||
UIModalService.show({
|
||||
content: CornerstoneViewportDownloadForm,
|
||||
title,
|
||||
contentProps: {
|
||||
activeViewportIndex,
|
||||
onClose: UIModalService.hide,
|
||||
},
|
||||
});
|
||||
});
|
||||
},
|
||||
nextImage: ({ viewports }) => {
|
||||
const enabledElement = _getActiveViewportEnabledElement(
|
||||
viewports.viewportSpecificData,
|
||||
viewports.activeViewportIndex
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
scroll(enabledElement, 1);
|
||||
},
|
||||
previousImage: ({ viewports }) => {
|
||||
const enabledElement = _getActiveViewportEnabledElement(
|
||||
viewports.viewportSpecificData,
|
||||
viewports.activeViewportIndex
|
||||
);
|
||||
const definitions = {
|
||||
showDownloadViewportModal: {
|
||||
commandFn: actions.showDownloadViewportModal,
|
||||
storeContexts: ['viewports'],
|
||||
options: {},
|
||||
},
|
||||
getActiveViewportEnabledElement: {
|
||||
commandFn: actions.getActiveViewportEnabledElement,
|
||||
storeContexts: ['viewports'],
|
||||
options: {},
|
||||
},
|
||||
rotateViewportCW: {
|
||||
commandFn: actions.rotateViewport,
|
||||
storeContexts: ['viewports'],
|
||||
options: { rotation: 90 },
|
||||
},
|
||||
rotateViewportCCW: {
|
||||
commandFn: actions.rotateViewport,
|
||||
storeContexts: ['viewports'],
|
||||
options: { rotation: -90 },
|
||||
},
|
||||
invertViewport: {
|
||||
commandFn: actions.invertViewport,
|
||||
storeContexts: ['viewports'],
|
||||
options: {},
|
||||
},
|
||||
flipViewportVertical: {
|
||||
commandFn: actions.flipViewportVertical,
|
||||
storeContexts: ['viewports'],
|
||||
options: {},
|
||||
},
|
||||
flipViewportHorizontal: {
|
||||
commandFn: actions.flipViewportHorizontal,
|
||||
storeContexts: ['viewports'],
|
||||
options: {},
|
||||
},
|
||||
scaleUpViewport: {
|
||||
commandFn: actions.scaleViewport,
|
||||
storeContexts: ['viewports'],
|
||||
options: { direction: 1 },
|
||||
},
|
||||
scaleDownViewport: {
|
||||
commandFn: actions.scaleViewport,
|
||||
storeContexts: ['viewports'],
|
||||
options: { direction: -1 },
|
||||
},
|
||||
fitViewportToWindow: {
|
||||
commandFn: actions.scaleViewport,
|
||||
storeContexts: ['viewports'],
|
||||
options: { direction: 0 },
|
||||
},
|
||||
resetViewport: {
|
||||
commandFn: actions.resetViewport,
|
||||
storeContexts: ['viewports'],
|
||||
options: {},
|
||||
},
|
||||
clearAnnotations: {
|
||||
commandFn: actions.clearAnnotations,
|
||||
storeContexts: ['viewports'],
|
||||
options: {},
|
||||
},
|
||||
nextImage: {
|
||||
commandFn: actions.nextImage,
|
||||
storeContexts: ['viewports'],
|
||||
options: {},
|
||||
},
|
||||
previousImage: {
|
||||
commandFn: actions.previousImage,
|
||||
storeContexts: ['viewports'],
|
||||
options: {},
|
||||
},
|
||||
// TODO: First/Last image
|
||||
// Next/Previous series/DisplaySet
|
||||
nextViewportDisplaySet: {
|
||||
commandFn: actions.updateViewportDisplaySet,
|
||||
storeContexts: [],
|
||||
options: { direction: 1 },
|
||||
},
|
||||
previousViewportDisplaySet: {
|
||||
commandFn: actions.updateViewportDisplaySet,
|
||||
storeContexts: [],
|
||||
options: { direction: -1 },
|
||||
},
|
||||
// TOOLS
|
||||
setToolActive: {
|
||||
commandFn: actions.setToolActive,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
};
|
||||
|
||||
scroll(enabledElement, -1);
|
||||
},
|
||||
return {
|
||||
actions,
|
||||
definitions,
|
||||
defaultContext: 'ACTIVE_VIEWPORT::CORNERSTONE',
|
||||
};
|
||||
};
|
||||
|
||||
const definitions = {
|
||||
rotateViewportCW: {
|
||||
commandFn: actions.rotateViewport,
|
||||
storeContexts: ['viewports'],
|
||||
options: { rotation: 90 },
|
||||
},
|
||||
rotateViewportCCW: {
|
||||
commandFn: actions.rotateViewport,
|
||||
storeContexts: ['viewports'],
|
||||
options: { rotation: -90 },
|
||||
},
|
||||
invertViewport: {
|
||||
commandFn: actions.invertViewport,
|
||||
storeContexts: ['viewports'],
|
||||
options: {},
|
||||
},
|
||||
flipViewportVertical: {
|
||||
commandFn: actions.flipViewportVertical,
|
||||
storeContexts: ['viewports'],
|
||||
options: {},
|
||||
},
|
||||
flipViewportHorizontal: {
|
||||
commandFn: actions.flipViewportHorizontal,
|
||||
storeContexts: ['viewports'],
|
||||
options: {},
|
||||
},
|
||||
scaleUpViewport: {
|
||||
commandFn: actions.scaleViewport,
|
||||
storeContexts: ['viewports'],
|
||||
options: { direction: 1 },
|
||||
},
|
||||
scaleDownViewport: {
|
||||
commandFn: actions.scaleViewport,
|
||||
storeContexts: ['viewports'],
|
||||
options: { direction: -1 },
|
||||
},
|
||||
fitViewportToWindow: {
|
||||
commandFn: actions.scaleViewport,
|
||||
storeContexts: ['viewports'],
|
||||
options: { direction: 0 },
|
||||
},
|
||||
resetViewport: {
|
||||
commandFn: actions.resetViewport,
|
||||
storeContexts: ['viewports'],
|
||||
options: {},
|
||||
},
|
||||
clearAnnotations: {
|
||||
commandFn: actions.clearAnnotations,
|
||||
storeContexts: ['viewports'],
|
||||
options: {},
|
||||
},
|
||||
nextImage: {
|
||||
commandFn: actions.nextImage,
|
||||
storeContexts: ['viewports'],
|
||||
options: {},
|
||||
},
|
||||
previousImage: {
|
||||
commandFn: actions.previousImage,
|
||||
storeContexts: ['viewports'],
|
||||
options: {},
|
||||
},
|
||||
// TODO: First/Last image
|
||||
// Next/Previous series/DisplaySet
|
||||
nextViewportDisplaySet: {
|
||||
commandFn: actions.updateViewportDisplaySet,
|
||||
storeContexts: [],
|
||||
options: { direction: 1 },
|
||||
},
|
||||
previousViewportDisplaySet: {
|
||||
commandFn: actions.updateViewportDisplaySet,
|
||||
storeContexts: [],
|
||||
options: { direction: -1 },
|
||||
},
|
||||
// TOOLS
|
||||
setToolActive: {
|
||||
commandFn: actions.setToolActive,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Grabs `dom` reference for the enabledElement of
|
||||
* the active viewport
|
||||
*/
|
||||
function _getActiveViewportEnabledElement(viewports, activeIndex) {
|
||||
const activeViewport = viewports[activeIndex] || {};
|
||||
return activeViewport.dom;
|
||||
}
|
||||
|
||||
export default {
|
||||
actions,
|
||||
definitions,
|
||||
defaultContext: 'ACTIVE_VIEWPORT::CORNERSTONE',
|
||||
};
|
||||
export default commandsModule;
|
||||
@@ -2,6 +2,7 @@ import init from './init.js';
|
||||
import asyncComponent from './asyncComponent.js';
|
||||
import commandsModule from './commandsModule.js';
|
||||
import toolbarModule from './toolbarModule.js';
|
||||
import CornerstoneViewportDownloadForm from './CornerstoneViewportDownloadForm';
|
||||
|
||||
const OHIFCornerstoneViewport = asyncComponent(() =>
|
||||
import(
|
||||
@@ -33,7 +34,9 @@ export default {
|
||||
getToolbarModule() {
|
||||
return toolbarModule;
|
||||
},
|
||||
getCommandsModule() {
|
||||
return commandsModule;
|
||||
getCommandsModule({ servicesManager }) {
|
||||
return commandsModule({ servicesManager });
|
||||
},
|
||||
};
|
||||
|
||||
export { CornerstoneViewportDownloadForm };
|
||||
@@ -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 });
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
const state = {
|
||||
enabledElements: {},
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets the enabled element `dom` reference for an active viewport.
|
||||
* @param {HTMLElement} dom Active viewport element.
|
||||
* @return void
|
||||
*/
|
||||
const setEnabledElement = (viewportIndex, element) =>
|
||||
(state.enabledElements[viewportIndex] = element);
|
||||
|
||||
/**
|
||||
* Grabs the enabled element `dom` reference of an active viewport.
|
||||
*
|
||||
* @return {HTMLElement} Active viewport element.
|
||||
*/
|
||||
const getEnabledElement = viewportIndex => state.enabledElements[viewportIndex];
|
||||
|
||||
export { setEnabledElement, getEnabledElement };
|
||||
@@ -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,14 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.0.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-html@1.0.1...@ohif/extension-dicom-html@1.0.2) (2019-12-02)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-html
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.0.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-html@1.0.0...@ohif/extension-dicom-html@1.0.1) (2019-10-29)
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/extension-dicom-html",
|
||||
"version": "1.0.1",
|
||||
"version": "1.0.2",
|
||||
"description": "OHIF extension for rendering structured reports to HTML",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -6,7 +6,7 @@ const { setViewportActive } = OHIF.redux.actions;
|
||||
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
const { viewportIndex, byteArray } = ownProps;
|
||||
const activeViewportIndex = state.viewports;
|
||||
const { activeViewportIndex } = state.viewports;
|
||||
|
||||
return {
|
||||
viewportIndex,
|
||||
|
||||
@@ -238,7 +238,7 @@ class DicomHtmlViewport extends Component {
|
||||
} = this.props;
|
||||
|
||||
if (viewportIndex !== activeViewportIndex) {
|
||||
setViewportActive();
|
||||
setViewportActive(viewportIndex);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -3,6 +3,81 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [0.54.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.54.1...@ohif/extension-vtk@0.54.2) (2019-12-02)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [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
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.53.7](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.53.6...@ohif/extension-vtk@0.53.7) (2019-11-15)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/extension-vtk",
|
||||
"version": "0.53.7",
|
||||
"version": "0.54.2",
|
||||
"description": "OHIF extension for VTK.js",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
@@ -52,9 +52,9 @@
|
||||
"react-vtkjs-viewport": "^0.3.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ohif/core": "^1.9.1",
|
||||
"@ohif/ui": "^0.62.2",
|
||||
"cornerstone-tools": "^4.0.9",
|
||||
"@ohif/core": "^1.13.2",
|
||||
"@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",
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import throttle from 'lodash.throttle';
|
||||
import {
|
||||
vtkInteractorStyleMPRCrosshairs,
|
||||
vtkInteractorStyleMPRWindowLevel,
|
||||
@@ -8,354 +9,358 @@ import {
|
||||
import setMPRLayout from './utils/setMPRLayout.js';
|
||||
import setViewportToVTK from './utils/setViewportToVTK.js';
|
||||
import Constants from 'vtk.js/Sources/Rendering/Core/VolumeMapper/Constants.js';
|
||||
import throttle from 'lodash.throttle';
|
||||
|
||||
const { BlendMode } = Constants;
|
||||
|
||||
// TODO: Put this somewhere else
|
||||
let apis = {};
|
||||
const commandsModule = ({ commandsManager }) => {
|
||||
// TODO: Put this somewhere else
|
||||
let apis = {};
|
||||
|
||||
async function _getActiveViewportVTKApi(viewports) {
|
||||
const {
|
||||
numRows,
|
||||
numColumns,
|
||||
layout,
|
||||
viewportSpecificData,
|
||||
activeViewportIndex,
|
||||
} = viewports;
|
||||
async function _getActiveViewportVTKApi(viewports) {
|
||||
const {
|
||||
numRows,
|
||||
numColumns,
|
||||
layout,
|
||||
viewportSpecificData,
|
||||
activeViewportIndex,
|
||||
} = viewports;
|
||||
|
||||
const currentData = layout.viewports[activeViewportIndex];
|
||||
if (currentData && currentData.plugin === 'vtk') {
|
||||
// TODO: I was storing/pulling this from Redux but ran into weird issues
|
||||
if (apis[activeViewportIndex]) {
|
||||
return apis[activeViewportIndex];
|
||||
const currentData = layout.viewports[activeViewportIndex];
|
||||
if (currentData && currentData.plugin === 'vtk') {
|
||||
// TODO: I was storing/pulling this from Redux but ran into weird issues
|
||||
if (apis[activeViewportIndex]) {
|
||||
return apis[activeViewportIndex];
|
||||
}
|
||||
}
|
||||
|
||||
const displaySet = viewportSpecificData[activeViewportIndex];
|
||||
|
||||
let api;
|
||||
if (!api) {
|
||||
try {
|
||||
api = await setViewportToVTK(
|
||||
displaySet,
|
||||
activeViewportIndex,
|
||||
numRows,
|
||||
numColumns,
|
||||
layout,
|
||||
viewportSpecificData
|
||||
);
|
||||
} catch (error) {
|
||||
throw new Error(error);
|
||||
}
|
||||
}
|
||||
|
||||
return api;
|
||||
}
|
||||
|
||||
const displaySet = viewportSpecificData[activeViewportIndex];
|
||||
function _setView(api, sliceNormal, viewUp) {
|
||||
const renderWindow = api.genericRenderWindow.getRenderWindow();
|
||||
const istyle = renderWindow.getInteractor().getInteractorStyle();
|
||||
istyle.setSliceNormal(...sliceNormal);
|
||||
istyle.setViewUp(...viewUp);
|
||||
|
||||
let api;
|
||||
if (!api) {
|
||||
try {
|
||||
api = await setViewportToVTK(
|
||||
displaySet,
|
||||
activeViewportIndex,
|
||||
numRows,
|
||||
numColumns,
|
||||
layout,
|
||||
viewportSpecificData
|
||||
renderWindow.render();
|
||||
}
|
||||
|
||||
function getVOIFromCornerstoneViewport() {
|
||||
const dom = commandsManager.runCommand('getActiveViewportEnabledElement');
|
||||
const cornerstoneElement = cornerstone.getEnabledElement(dom);
|
||||
|
||||
if (cornerstoneElement) {
|
||||
const imageId = cornerstoneElement.image.imageId;
|
||||
|
||||
const { modality } = cornerstone.metaData.get(
|
||||
'generalSeriesModule',
|
||||
imageId
|
||||
);
|
||||
} catch (error) {
|
||||
throw new Error(error);
|
||||
|
||||
if (modality !== 'PT') {
|
||||
const { windowWidth, windowCenter } = cornerstoneElement.viewport.voi;
|
||||
|
||||
return {
|
||||
windowWidth,
|
||||
windowCenter,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return api;
|
||||
}
|
||||
function setVOI(voi) {
|
||||
const { windowWidth, windowCenter } = voi;
|
||||
const lower = windowCenter - windowWidth / 2.0;
|
||||
const upper = windowCenter + windowWidth / 2.0;
|
||||
|
||||
function _setView(api, sliceNormal, viewUp) {
|
||||
const renderWindow = api.genericRenderWindow.getRenderWindow();
|
||||
const istyle = renderWindow.getInteractor().getInteractorStyle();
|
||||
istyle.setSliceNormal(...sliceNormal);
|
||||
istyle.setViewUp(...viewUp);
|
||||
const rgbTransferFunction = apis[0].volumes[0]
|
||||
.getProperty()
|
||||
.getRGBTransferFunction(0);
|
||||
|
||||
renderWindow.render();
|
||||
}
|
||||
rgbTransferFunction.setRange(lower, upper);
|
||||
|
||||
const actions = {
|
||||
axial: async ({ viewports }) => {
|
||||
const api = await _getActiveViewportVTKApi(viewports);
|
||||
|
||||
apis[viewports.activeViewportIndex] = api;
|
||||
|
||||
_setView(api, [0, 0, 1], [0, -1, 0]);
|
||||
},
|
||||
sagittal: async ({ viewports }) => {
|
||||
const api = await _getActiveViewportVTKApi(viewports);
|
||||
|
||||
apis[viewports.activeViewportIndex] = api;
|
||||
|
||||
_setView(api, [1, 0, 0], [0, 0, 1]);
|
||||
},
|
||||
coronal: async ({ viewports }) => {
|
||||
const api = await _getActiveViewportVTKApi(viewports);
|
||||
|
||||
apis[viewports.activeViewportIndex] = api;
|
||||
|
||||
_setView(api, [0, 1, 0], [0, 0, 1]);
|
||||
},
|
||||
enableRotateTool: () => {
|
||||
apis.forEach(api => {
|
||||
const istyle = vtkInteractorStyleMPRRotate.newInstance();
|
||||
|
||||
api.setInteractorStyle({ istyle });
|
||||
api.updateVOI(windowWidth, windowCenter);
|
||||
});
|
||||
},
|
||||
enableCrosshairsTool: () => {
|
||||
apis.forEach((api, apiIndex) => {
|
||||
const istyle = vtkInteractorStyleMPRCrosshairs.newInstance();
|
||||
}
|
||||
|
||||
api.setInteractorStyle({
|
||||
istyle,
|
||||
configuration: { apis, apiIndex },
|
||||
});
|
||||
});
|
||||
},
|
||||
enableLevelTool: () => {
|
||||
function updateVOI(apis, windowWidth, windowCenter) {
|
||||
const actions = {
|
||||
axial: async ({ viewports }) => {
|
||||
const api = await _getActiveViewportVTKApi(viewports);
|
||||
|
||||
apis[viewports.activeViewportIndex] = api;
|
||||
|
||||
_setView(api, [0, 0, 1], [0, -1, 0]);
|
||||
},
|
||||
sagittal: async ({ viewports }) => {
|
||||
const api = await _getActiveViewportVTKApi(viewports);
|
||||
|
||||
apis[viewports.activeViewportIndex] = api;
|
||||
|
||||
_setView(api, [1, 0, 0], [0, 0, 1]);
|
||||
},
|
||||
coronal: async ({ viewports }) => {
|
||||
const api = await _getActiveViewportVTKApi(viewports);
|
||||
|
||||
apis[viewports.activeViewportIndex] = api;
|
||||
|
||||
_setView(api, [0, 1, 0], [0, 0, 1]);
|
||||
},
|
||||
enableRotateTool: () => {
|
||||
apis.forEach(api => {
|
||||
api.updateVOI(windowWidth, windowCenter);
|
||||
const istyle = vtkInteractorStyleMPRRotate.newInstance();
|
||||
|
||||
api.setInteractorStyle({ istyle });
|
||||
});
|
||||
}
|
||||
},
|
||||
enableCrosshairsTool: () => {
|
||||
apis.forEach((api, apiIndex) => {
|
||||
const istyle = vtkInteractorStyleMPRCrosshairs.newInstance();
|
||||
|
||||
const throttledUpdateVOIs = throttle(updateVOI, 16, { trailing: true }); // ~ 60 fps
|
||||
|
||||
const callbacks = {
|
||||
setOnLevelsChanged: ({ windowCenter, windowWidth }) => {
|
||||
apis.forEach(api => {
|
||||
const renderWindow = api.genericRenderWindow.getRenderWindow();
|
||||
|
||||
renderWindow.render();
|
||||
api.setInteractorStyle({
|
||||
istyle,
|
||||
configuration: { apis, apiIndex },
|
||||
});
|
||||
|
||||
throttledUpdateVOIs(apis, windowWidth, windowCenter);
|
||||
},
|
||||
};
|
||||
|
||||
apis.forEach(api => {
|
||||
const istyle = vtkInteractorStyleMPRWindowLevel.newInstance();
|
||||
|
||||
api.setInteractorStyle({ istyle, callbacks });
|
||||
});
|
||||
},
|
||||
setSlabThickness: ({ slabThickness }) => {
|
||||
apis.forEach(api => {
|
||||
api.setSlabThickness(slabThickness);
|
||||
});
|
||||
},
|
||||
changeSlabThickness: ({ change }) => {
|
||||
apis.forEach(api => {
|
||||
const slabThickness = Math.max(api.getSlabThickness() + change, 0.1);
|
||||
|
||||
api.setSlabThickness(slabThickness);
|
||||
});
|
||||
},
|
||||
setBlendModeToComposite: () => {
|
||||
apis.forEach(api => {
|
||||
const renderWindow = api.genericRenderWindow.getRenderWindow();
|
||||
const istyle = renderWindow.getInteractor().getInteractorStyle();
|
||||
|
||||
const slabThickness = api.getSlabThickness();
|
||||
|
||||
const mapper = api.volumes[0].getMapper();
|
||||
if (mapper.setBlendModeToComposite) {
|
||||
mapper.setBlendModeToComposite();
|
||||
}
|
||||
|
||||
if (istyle.setSlabThickness) {
|
||||
istyle.setSlabThickness(slabThickness);
|
||||
}
|
||||
renderWindow.render();
|
||||
});
|
||||
},
|
||||
setBlendModeToMaximumIntensity: () => {
|
||||
apis.forEach(api => {
|
||||
const renderWindow = api.genericRenderWindow.getRenderWindow();
|
||||
const mapper = api.volumes[0].getMapper();
|
||||
if (mapper.setBlendModeToMaximumIntensity) {
|
||||
mapper.setBlendModeToMaximumIntensity();
|
||||
}
|
||||
renderWindow.render();
|
||||
});
|
||||
},
|
||||
setBlendMode: ({ blendMode }) => {
|
||||
apis.forEach(api => {
|
||||
const renderWindow = api.genericRenderWindow.getRenderWindow();
|
||||
|
||||
api.volumes[0].getMapper().setBlendMode(blendMode);
|
||||
|
||||
renderWindow.render();
|
||||
});
|
||||
},
|
||||
mpr2d: async ({ viewports }) => {
|
||||
// TODO push a lot of this backdoor logic lower down to the library level.
|
||||
const displaySet =
|
||||
viewports.viewportSpecificData[viewports.activeViewportIndex];
|
||||
|
||||
// Get current VOI if cornerstone viewport.
|
||||
const cornerstoneVOI = getVOIFromCornerstoneViewport(displaySet);
|
||||
|
||||
const viewportProps = [
|
||||
{
|
||||
//Axial
|
||||
orientation: {
|
||||
sliceNormal: [0, 0, 1],
|
||||
viewUp: [0, -1, 0],
|
||||
},
|
||||
},
|
||||
{
|
||||
// Sagital
|
||||
orientation: {
|
||||
sliceNormal: [1, 0, 0],
|
||||
viewUp: [0, 0, 1],
|
||||
},
|
||||
},
|
||||
{
|
||||
// Coronal
|
||||
orientation: {
|
||||
sliceNormal: [0, 1, 0],
|
||||
viewUp: [0, 0, 1],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
try {
|
||||
apis = await setMPRLayout(displaySet, viewportProps, 1, 3);
|
||||
} catch (error) {
|
||||
throw new Error(error);
|
||||
}
|
||||
|
||||
if (cornerstoneVOI) {
|
||||
setVOI(cornerstoneVOI);
|
||||
}
|
||||
|
||||
// Add widgets and set default interactorStyle of each viewport.
|
||||
apis.forEach((api, apiIndex) => {
|
||||
api.addSVGWidget(
|
||||
vtkSVGCrosshairsWidget.newInstance(),
|
||||
'crosshairsWidget'
|
||||
);
|
||||
|
||||
const istyle = vtkInteractorStyleMPRCrosshairs.newInstance();
|
||||
|
||||
api.setInteractorStyle({
|
||||
istyle,
|
||||
configuration: { apis, apiIndex },
|
||||
});
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
window.vtkActions = actions;
|
||||
|
||||
const definitions = {
|
||||
axial: {
|
||||
commandFn: actions.axial,
|
||||
storeContexts: ['viewports'],
|
||||
options: {},
|
||||
},
|
||||
coronal: {
|
||||
commandFn: actions.coronal,
|
||||
storeContexts: ['viewports'],
|
||||
options: {},
|
||||
},
|
||||
sagittal: {
|
||||
commandFn: actions.sagittal,
|
||||
storeContexts: ['viewports'],
|
||||
options: {},
|
||||
},
|
||||
enableRotateTool: {
|
||||
commandFn: actions.enableRotateTool,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
enableCrosshairsTool: {
|
||||
commandFn: actions.enableCrosshairsTool,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
enableLevelTool: {
|
||||
commandFn: actions.enableLevelTool,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
setBlendModeToComposite: {
|
||||
commandFn: actions.setBlendModeToComposite,
|
||||
storeContexts: [],
|
||||
options: { blendMode: BlendMode.COMPOSITE_BLEND },
|
||||
},
|
||||
setBlendModeToMaximumIntensity: {
|
||||
commandFn: actions.setBlendModeToMaximumIntensity,
|
||||
storeContexts: [],
|
||||
options: { blendMode: BlendMode.MAXIMUM_INTENSITY_BLEND },
|
||||
},
|
||||
setBlendModeToMinimumIntensity: {
|
||||
commandFn: actions.setBlendMode,
|
||||
storeContexts: [],
|
||||
options: { blendMode: BlendMode.MINIMUM_INTENSITY_BLEND },
|
||||
},
|
||||
setBlendModeToAverageIntensity: {
|
||||
commandFn: actions.setBlendMode,
|
||||
storeContexts: [],
|
||||
options: { blendMode: BlendMode.AVERAGE_INTENSITY_BLEND },
|
||||
},
|
||||
setSlabThickness: {
|
||||
// TODO: How do we pass in a function argument?
|
||||
commandFn: actions.setSlabThickness,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
increaseSlabThickness: {
|
||||
commandFn: actions.changeSlabThickness,
|
||||
storeContexts: [],
|
||||
options: {
|
||||
change: 3,
|
||||
},
|
||||
},
|
||||
decreaseSlabThickness: {
|
||||
commandFn: actions.changeSlabThickness,
|
||||
storeContexts: [],
|
||||
options: {
|
||||
change: -3,
|
||||
},
|
||||
},
|
||||
mpr2d: {
|
||||
commandFn: actions.mpr2d,
|
||||
storeContexts: ['viewports'],
|
||||
options: {},
|
||||
context: 'VIEWER',
|
||||
},
|
||||
};
|
||||
enableLevelTool: () => {
|
||||
function updateVOI(apis, windowWidth, windowCenter) {
|
||||
apis.forEach(api => {
|
||||
api.updateVOI(windowWidth, windowCenter);
|
||||
});
|
||||
}
|
||||
|
||||
export default {
|
||||
definitions,
|
||||
defaultContext: 'ACTIVE_VIEWPORT::VTK',
|
||||
};
|
||||
const throttledUpdateVOIs = throttle(updateVOI, 16, { trailing: true }); // ~ 60 fps
|
||||
|
||||
function getVOIFromCornerstoneViewport(displaySet) {
|
||||
const cornerstoneElement = cornerstone.getEnabledElement(displaySet.dom);
|
||||
const callbacks = {
|
||||
setOnLevelsChanged: ({ windowCenter, windowWidth }) => {
|
||||
apis.forEach(api => {
|
||||
const renderWindow = api.genericRenderWindow.getRenderWindow();
|
||||
|
||||
if (cornerstoneElement) {
|
||||
const imageId = cornerstoneElement.image.imageId;
|
||||
renderWindow.render();
|
||||
});
|
||||
|
||||
const { modality } = cornerstone.metaData.get(
|
||||
'generalSeriesModule',
|
||||
imageId
|
||||
);
|
||||
|
||||
if (modality !== 'PT') {
|
||||
const { windowWidth, windowCenter } = cornerstoneElement.viewport.voi;
|
||||
|
||||
return {
|
||||
windowWidth,
|
||||
windowCenter,
|
||||
throttledUpdateVOIs(apis, windowWidth, windowCenter);
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setVOI(voi) {
|
||||
const { windowWidth, windowCenter } = voi;
|
||||
const lower = windowCenter - windowWidth / 2.0;
|
||||
const upper = windowCenter + windowWidth / 2.0;
|
||||
apis.forEach(api => {
|
||||
const istyle = vtkInteractorStyleMPRWindowLevel.newInstance();
|
||||
|
||||
const rgbTransferFunction = apis[0].volumes[0]
|
||||
.getProperty()
|
||||
.getRGBTransferFunction(0);
|
||||
api.setInteractorStyle({ istyle, callbacks });
|
||||
});
|
||||
},
|
||||
setSlabThickness: ({ slabThickness }) => {
|
||||
apis.forEach(api => {
|
||||
api.setSlabThickness(slabThickness);
|
||||
});
|
||||
},
|
||||
changeSlabThickness: ({ change }) => {
|
||||
apis.forEach(api => {
|
||||
const slabThickness = Math.max(api.getSlabThickness() + change, 0.1);
|
||||
|
||||
rgbTransferFunction.setRange(lower, upper);
|
||||
api.setSlabThickness(slabThickness);
|
||||
});
|
||||
},
|
||||
setBlendModeToComposite: () => {
|
||||
apis.forEach(api => {
|
||||
const renderWindow = api.genericRenderWindow.getRenderWindow();
|
||||
const istyle = renderWindow.getInteractor().getInteractorStyle();
|
||||
|
||||
apis.forEach(api => {
|
||||
api.updateVOI(windowWidth, windowCenter);
|
||||
});
|
||||
}
|
||||
const slabThickness = api.getSlabThickness();
|
||||
|
||||
const mapper = api.volumes[0].getMapper();
|
||||
if (mapper.setBlendModeToComposite) {
|
||||
mapper.setBlendModeToComposite();
|
||||
}
|
||||
|
||||
if (istyle.setSlabThickness) {
|
||||
istyle.setSlabThickness(slabThickness);
|
||||
}
|
||||
renderWindow.render();
|
||||
});
|
||||
},
|
||||
setBlendModeToMaximumIntensity: () => {
|
||||
apis.forEach(api => {
|
||||
const renderWindow = api.genericRenderWindow.getRenderWindow();
|
||||
const mapper = api.volumes[0].getMapper();
|
||||
if (mapper.setBlendModeToMaximumIntensity) {
|
||||
mapper.setBlendModeToMaximumIntensity();
|
||||
}
|
||||
renderWindow.render();
|
||||
});
|
||||
},
|
||||
setBlendMode: ({ blendMode }) => {
|
||||
apis.forEach(api => {
|
||||
const renderWindow = api.genericRenderWindow.getRenderWindow();
|
||||
|
||||
api.volumes[0].getMapper().setBlendMode(blendMode);
|
||||
|
||||
renderWindow.render();
|
||||
});
|
||||
},
|
||||
mpr2d: async ({ viewports }) => {
|
||||
// TODO push a lot of this backdoor logic lower down to the library level.
|
||||
const displaySet =
|
||||
viewports.viewportSpecificData[viewports.activeViewportIndex];
|
||||
|
||||
// Get current VOI if cornerstone viewport.
|
||||
const cornerstoneVOI = getVOIFromCornerstoneViewport();
|
||||
|
||||
const viewportProps = [
|
||||
{
|
||||
//Axial
|
||||
orientation: {
|
||||
sliceNormal: [0, 0, 1],
|
||||
viewUp: [0, -1, 0],
|
||||
},
|
||||
},
|
||||
{
|
||||
// Sagital
|
||||
orientation: {
|
||||
sliceNormal: [1, 0, 0],
|
||||
viewUp: [0, 0, 1],
|
||||
},
|
||||
},
|
||||
{
|
||||
// Coronal
|
||||
orientation: {
|
||||
sliceNormal: [0, 1, 0],
|
||||
viewUp: [0, 0, 1],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
try {
|
||||
apis = await setMPRLayout(displaySet, viewportProps, 1, 3);
|
||||
} catch (error) {
|
||||
throw new Error(error);
|
||||
}
|
||||
|
||||
if (cornerstoneVOI) {
|
||||
setVOI(cornerstoneVOI);
|
||||
}
|
||||
|
||||
// Add widgets and set default interactorStyle of each viewport.
|
||||
apis.forEach((api, apiIndex) => {
|
||||
api.addSVGWidget(
|
||||
vtkSVGCrosshairsWidget.newInstance(),
|
||||
'crosshairsWidget'
|
||||
);
|
||||
|
||||
const istyle = vtkInteractorStyleMPRCrosshairs.newInstance();
|
||||
|
||||
api.setInteractorStyle({
|
||||
istyle,
|
||||
configuration: { apis, apiIndex },
|
||||
});
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
window.vtkActions = actions;
|
||||
|
||||
const definitions = {
|
||||
axial: {
|
||||
commandFn: actions.axial,
|
||||
storeContexts: ['viewports'],
|
||||
options: {},
|
||||
},
|
||||
coronal: {
|
||||
commandFn: actions.coronal,
|
||||
storeContexts: ['viewports'],
|
||||
options: {},
|
||||
},
|
||||
sagittal: {
|
||||
commandFn: actions.sagittal,
|
||||
storeContexts: ['viewports'],
|
||||
options: {},
|
||||
},
|
||||
enableRotateTool: {
|
||||
commandFn: actions.enableRotateTool,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
enableCrosshairsTool: {
|
||||
commandFn: actions.enableCrosshairsTool,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
enableLevelTool: {
|
||||
commandFn: actions.enableLevelTool,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
setBlendModeToComposite: {
|
||||
commandFn: actions.setBlendModeToComposite,
|
||||
storeContexts: [],
|
||||
options: { blendMode: BlendMode.COMPOSITE_BLEND },
|
||||
},
|
||||
setBlendModeToMaximumIntensity: {
|
||||
commandFn: actions.setBlendModeToMaximumIntensity,
|
||||
storeContexts: [],
|
||||
options: { blendMode: BlendMode.MAXIMUM_INTENSITY_BLEND },
|
||||
},
|
||||
setBlendModeToMinimumIntensity: {
|
||||
commandFn: actions.setBlendMode,
|
||||
storeContexts: [],
|
||||
options: { blendMode: BlendMode.MINIMUM_INTENSITY_BLEND },
|
||||
},
|
||||
setBlendModeToAverageIntensity: {
|
||||
commandFn: actions.setBlendMode,
|
||||
storeContexts: [],
|
||||
options: { blendMode: BlendMode.AVERAGE_INTENSITY_BLEND },
|
||||
},
|
||||
setSlabThickness: {
|
||||
// TODO: How do we pass in a function argument?
|
||||
commandFn: actions.setSlabThickness,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
increaseSlabThickness: {
|
||||
commandFn: actions.changeSlabThickness,
|
||||
storeContexts: [],
|
||||
options: {
|
||||
change: 3,
|
||||
},
|
||||
},
|
||||
decreaseSlabThickness: {
|
||||
commandFn: actions.changeSlabThickness,
|
||||
storeContexts: [],
|
||||
options: {
|
||||
change: -3,
|
||||
},
|
||||
},
|
||||
mpr2d: {
|
||||
commandFn: actions.mpr2d,
|
||||
storeContexts: ['viewports'],
|
||||
options: {},
|
||||
context: 'VIEWER',
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
definitions,
|
||||
defaultContext: 'ACTIVE_VIEWPORT::VTK',
|
||||
};
|
||||
};
|
||||
|
||||
export default commandsModule;
|
||||
@@ -20,8 +20,8 @@ const vtkExtension = {
|
||||
getToolbarModule() {
|
||||
return toolbarModule;
|
||||
},
|
||||
getCommandsModule() {
|
||||
return commandsModule;
|
||||
getCommandsModule({ commandsManager }) {
|
||||
return commandsModule({ commandsManager });
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -3,6 +3,69 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.13.2](https://github.com/OHIF/Viewers/compare/@ohif/core@1.13.1...@ohif/core@1.13.2) (2019-12-02)
|
||||
|
||||
**Note:** Version bump only for package @ohif/core
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [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)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Inject into Extension Modules / improve tests ([f63d8a7](https://github.com/OHIF/Viewers/commit/f63d8a73d867ad9dfd8ee0cad74edce180eb34f0))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.9.1](https://github.com/OHIF/Viewers/compare/@ohif/core@1.9.0...@ohif/core@1.9.1) (2019-11-15)
|
||||
|
||||
**Note:** Version bump only for package @ohif/core
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/core",
|
||||
"version": "1.9.1",
|
||||
"version": "1.13.2",
|
||||
"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);
|
||||
});
|
||||
|
||||
@@ -69,6 +69,7 @@ export default class ExtensionManager {
|
||||
if (extension.preRegistration) {
|
||||
extension.preRegistration({
|
||||
servicesManager: this._servicesManager,
|
||||
commandsManager: this._commandsManager,
|
||||
configuration,
|
||||
});
|
||||
}
|
||||
@@ -110,7 +111,10 @@ export default class ExtensionManager {
|
||||
}
|
||||
|
||||
try {
|
||||
const extensionModule = getModuleFn();
|
||||
const extensionModule = getModuleFn({
|
||||
servicesManager: this._servicesManager,
|
||||
commandsManager: this._commandsManager,
|
||||
});
|
||||
|
||||
if (!extensionModule) {
|
||||
log.warn(
|
||||
|
||||
@@ -40,6 +40,31 @@ describe('ExtensionManager.js', () => {
|
||||
});
|
||||
|
||||
describe('registerExtension()', () => {
|
||||
it('calls preRegistration() for extension', () => {
|
||||
// SUT
|
||||
const fakeExtension = { one: '1', preRegistration: jest.fn() };
|
||||
extensionManager.registerExtension(fakeExtension);
|
||||
|
||||
// Assert
|
||||
expect(fakeExtension.preRegistration.mock.calls.length).toBe(1);
|
||||
});
|
||||
|
||||
it('calls preRegistration() passing configuration along with servicesManager and commandsManager instances for extension', () => {
|
||||
const configuration = { config: 'Some configuration' };
|
||||
extensionManager._servicesManager = { services: { TestService: {} } };
|
||||
|
||||
// SUT
|
||||
const fakeExtension = { one: '1', preRegistration: jest.fn() };
|
||||
extensionManager.registerExtension(fakeExtension, configuration);
|
||||
|
||||
// Assert
|
||||
expect(fakeExtension.preRegistration.mock.calls[0][0]).toEqual({
|
||||
servicesManager: extensionManager._servicesManager,
|
||||
commandsManager: extensionManager._commandsManager,
|
||||
configuration,
|
||||
});
|
||||
});
|
||||
|
||||
it('logs a warning if the extension is null or undefined', () => {
|
||||
const undefinedExtension = undefined;
|
||||
const nullExtension = null;
|
||||
@@ -110,6 +135,26 @@ describe('ExtensionManager.js', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('successfully passes a servicesManager and commandsManager instances to each module', () => {
|
||||
extensionManager._servicesManager = { services: { TestService: {} } };
|
||||
|
||||
const extension = {
|
||||
id: 'hello-world',
|
||||
getViewportModule: jest.fn(),
|
||||
getSopClassHandlerModule: jest.fn(),
|
||||
getPanelModule: jest.fn(),
|
||||
getToolbarModule: jest.fn(),
|
||||
getCommandsModule: jest.fn(),
|
||||
};
|
||||
|
||||
extensionManager.registerExtension(extension);
|
||||
|
||||
expect(extension.getViewportModule.mock.calls[0][0]).toEqual({
|
||||
servicesManager: extensionManager._servicesManager,
|
||||
commandsManager: extensionManager._commandsManager,
|
||||
});
|
||||
});
|
||||
|
||||
it('successfully registers a module for each module type', () => {
|
||||
const extension = {
|
||||
id: 'hello-world',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -12,13 +12,25 @@ import {
|
||||
SET_VIEWPORT_LAYOUT_AND_DATA,
|
||||
} from './constants/ActionTypes.js';
|
||||
|
||||
/**
|
||||
* The definition of a viewport layout.
|
||||
*
|
||||
* @typedef {Object} ViewportLayout
|
||||
* @property {number} numRows -
|
||||
* @property {number} numColumns -
|
||||
* @property {array} viewports -
|
||||
*/
|
||||
|
||||
/**
|
||||
* VIEWPORT
|
||||
*/
|
||||
export const setViewportSpecificData = (viewportIndex, data) => ({
|
||||
export const setViewportSpecificData = (
|
||||
viewportIndex,
|
||||
viewportSpecificData
|
||||
) => ({
|
||||
type: SET_VIEWPORT,
|
||||
viewportIndex,
|
||||
data,
|
||||
viewportSpecificData,
|
||||
});
|
||||
|
||||
export const setViewportActive = viewportIndex => ({
|
||||
@@ -27,10 +39,7 @@ export const setViewportActive = viewportIndex => ({
|
||||
});
|
||||
|
||||
/**
|
||||
* @param {object} layout
|
||||
* @param {number} layout.numRows
|
||||
* @param {number} layout.numColumns
|
||||
* @param {array} layout.viewports
|
||||
* @param {ViewportLayout} layout
|
||||
*/
|
||||
export const setLayout = ({ numRows, numColumns, viewports }) => ({
|
||||
type: SET_VIEWPORT_LAYOUT,
|
||||
@@ -40,10 +49,9 @@ export const setLayout = ({ numRows, numColumns, viewports }) => ({
|
||||
});
|
||||
|
||||
/**
|
||||
* @param {object} layout
|
||||
* @param {number} layout.numRows
|
||||
* @param {number} layout.numColumns
|
||||
* @param {array} layout.viewports
|
||||
* @param {array} viewports
|
||||
*/
|
||||
export const setViewportLayoutAndData = (
|
||||
{ numRows, numColumns, viewports },
|
||||
@@ -61,9 +69,9 @@ export const clearViewportSpecificData = viewportIndex => ({
|
||||
viewportIndex,
|
||||
});
|
||||
|
||||
export const setActiveViewportSpecificData = data => ({
|
||||
export const setActiveViewportSpecificData = viewportSpecificData => ({
|
||||
type: SET_ACTIVE_SPECIFIC_DATA,
|
||||
data,
|
||||
viewportSpecificData,
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -113,13 +121,18 @@ export const setServers = servers => ({
|
||||
});
|
||||
|
||||
const actions = {
|
||||
// VIEWPORT
|
||||
/**
|
||||
* VIEWPORT
|
||||
*/
|
||||
setViewportActive,
|
||||
setViewportSpecificData,
|
||||
setViewportLayoutAndData,
|
||||
setLayout,
|
||||
clearViewportSpecificData,
|
||||
setActiveViewportSpecificData,
|
||||
/**
|
||||
* NOT-VIEWPORT
|
||||
*/
|
||||
setStudyLoadingProgress,
|
||||
clearStudyLoadingProgress,
|
||||
setUserPreferences,
|
||||
|
||||
@@ -28,7 +28,7 @@ describe('actions', () => {
|
||||
|
||||
describe('viewport action creators', () => {
|
||||
it('should create an action to set the viewport specific data', () => {
|
||||
const data = {
|
||||
const viewportSpecificData = {
|
||||
displaySetInstanceUid: 'ef859a23-4631-93ab-d26b-7940a822c699',
|
||||
seriesDate: '20151026',
|
||||
seriesTime: '082611.370000',
|
||||
@@ -65,12 +65,12 @@ describe('actions', () => {
|
||||
|
||||
const expectedAction = {
|
||||
type: types.SET_ACTIVE_SPECIFIC_DATA,
|
||||
data,
|
||||
viewportSpecificData,
|
||||
};
|
||||
|
||||
expect(actions.setActiveViewportSpecificData(data)).toEqual(
|
||||
expectedAction
|
||||
);
|
||||
expect(
|
||||
actions.setActiveViewportSpecificData(viewportSpecificData)
|
||||
).toEqual(expectedAction);
|
||||
});
|
||||
|
||||
it('should create an action to clear clearViewportSpecificData', () => {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import cloneDeep from 'lodash.clonedeep';
|
||||
import merge from 'lodash.merge';
|
||||
|
||||
import {
|
||||
CLEAR_VIEWPORT,
|
||||
SET_ACTIVE_SPECIFIC_DATA,
|
||||
@@ -8,10 +11,7 @@ import {
|
||||
SET_VIEWPORT_LAYOUT_AND_DATA,
|
||||
} from './../constants/ActionTypes.js';
|
||||
|
||||
import cloneDeep from 'lodash.clonedeep';
|
||||
import merge from 'lodash.merge';
|
||||
|
||||
const defaultState = {
|
||||
const DEFAULT_STATE = {
|
||||
numRows: 1,
|
||||
numColumns: 1,
|
||||
activeViewportIndex: 0,
|
||||
@@ -26,94 +26,137 @@ const defaultState = {
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {Object} [state=defaultState]
|
||||
* @param {Object} action
|
||||
* @param {string} [action.type]
|
||||
* @param {number} [action.viewportIndex]
|
||||
* @param {Object} [action.layout]
|
||||
* @param {Object} [action.viewportSpecificData]
|
||||
* The definition of a viewport action.
|
||||
*
|
||||
* @typedef {Object} ViewportAction
|
||||
* @property {string} type -
|
||||
* @property {Object} data -
|
||||
* @property {Object} layout -
|
||||
* @property {number} viewportIndex -
|
||||
* @property {Object} viewportSpecificData -
|
||||
*/
|
||||
const viewports = (state = defaultState, action) => {
|
||||
let viewportSpecificData;
|
||||
|
||||
/**
|
||||
* @param {Object} [state=DEFAULT_STATE] The current viewport state.
|
||||
* @param {ViewportAction} action A viewport action.
|
||||
*/
|
||||
const viewports = (state = DEFAULT_STATE, action) => {
|
||||
let useActiveViewport = false;
|
||||
|
||||
switch (action.type) {
|
||||
case SET_VIEWPORT_ACTIVE:
|
||||
return Object.assign({}, state, {
|
||||
activeViewportIndex: action.viewportIndex,
|
||||
});
|
||||
/**
|
||||
* Sets the active viewport index.
|
||||
*
|
||||
* @return {Object} New state.
|
||||
*/
|
||||
case SET_VIEWPORT_ACTIVE: {
|
||||
return { ...state, activeViewportIndex: action.viewportIndex };
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets viewport layout.
|
||||
*
|
||||
* @return {Object} New state.
|
||||
*/
|
||||
case SET_VIEWPORT_LAYOUT: {
|
||||
const { numRows, numColumns, viewports } = action;
|
||||
const layout = {
|
||||
viewports: [...viewports],
|
||||
return {
|
||||
...state,
|
||||
numRows: action.numRows,
|
||||
numColumns: action.numColumns,
|
||||
layout: { viewports: [...action.viewports] },
|
||||
};
|
||||
|
||||
return Object.assign({}, state, { numRows, numColumns, layout });
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets viewport layout and data.
|
||||
*
|
||||
* @return {Object} New state.
|
||||
*/
|
||||
case SET_VIEWPORT_LAYOUT_AND_DATA: {
|
||||
const { numRows, numColumns, viewports, viewportSpecificData } = action;
|
||||
const layout = {
|
||||
viewports: [...viewports],
|
||||
return {
|
||||
...state,
|
||||
numRows: action.numRows,
|
||||
numColumns: action.numColumns,
|
||||
layout: { viewports: [...action.viewports] },
|
||||
viewportSpecificData: cloneDeep(action.viewportSpecificData),
|
||||
};
|
||||
|
||||
return Object.assign({}, state, {
|
||||
numRows,
|
||||
numColumns,
|
||||
layout,
|
||||
viewportSpecificData: cloneDeep(viewportSpecificData),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets viewport specific data of active viewport.
|
||||
*
|
||||
* @return {Object} New state.
|
||||
*/
|
||||
case SET_VIEWPORT: {
|
||||
const layout = cloneDeep(state.layout);
|
||||
const hasPlugin = action.data && action.data.plugin;
|
||||
|
||||
viewportSpecificData = cloneDeep(state.viewportSpecificData);
|
||||
let viewportSpecificData = cloneDeep(state.viewportSpecificData);
|
||||
viewportSpecificData[action.viewportIndex] = merge(
|
||||
{},
|
||||
viewportSpecificData[action.viewportIndex],
|
||||
action.data
|
||||
action.viewportSpecificData
|
||||
);
|
||||
|
||||
if (hasPlugin) {
|
||||
layout.viewports[action.viewportIndex].plugin = action.data.plugin;
|
||||
if (action.viewportSpecificData && action.viewportSpecificData.plugin) {
|
||||
layout.viewports[action.viewportIndex].plugin =
|
||||
action.viewportSpecificData.plugin;
|
||||
}
|
||||
|
||||
return Object.assign({}, state, { layout, viewportSpecificData });
|
||||
return { ...state, layout, viewportSpecificData };
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets viewport specific data of active/any viewport.
|
||||
*
|
||||
* @return {Object} New state.
|
||||
*/
|
||||
case SET_ACTIVE_SPECIFIC_DATA:
|
||||
useActiveViewport = true;
|
||||
// Allow fall-through
|
||||
// eslint-disable-next-line
|
||||
case SET_SPECIFIC_DATA: {
|
||||
const layout = cloneDeep(state.layout);
|
||||
const hasPlugin = action.data && action.data.plugin;
|
||||
const viewportIndex = useActiveViewport
|
||||
? state.activeViewportIndex
|
||||
: action.viewportIndex;
|
||||
const { dom } = state.viewportSpecificData[viewportIndex];
|
||||
|
||||
viewportSpecificData = cloneDeep(state.viewportSpecificData);
|
||||
let viewportSpecificData = cloneDeep(state.viewportSpecificData);
|
||||
viewportSpecificData[viewportIndex] = {
|
||||
dom,
|
||||
...action.data,
|
||||
...action.viewportSpecificData,
|
||||
};
|
||||
|
||||
if (hasPlugin) {
|
||||
layout.viewports[viewportIndex].plugin = action.data.plugin;
|
||||
if (action.viewportSpecificData && action.viewportSpecificData.plugin) {
|
||||
layout.viewports[viewportIndex].plugin =
|
||||
action.viewportSpecificData.plugin;
|
||||
}
|
||||
|
||||
return Object.assign({}, state, { layout, viewportSpecificData });
|
||||
return { ...state, layout, viewportSpecificData };
|
||||
}
|
||||
case CLEAR_VIEWPORT:
|
||||
viewportSpecificData = cloneDeep(state.viewportSpecificData);
|
||||
|
||||
/**
|
||||
* Clears viewport specific data of any viewport.
|
||||
*
|
||||
* @return {Object} New state.
|
||||
*/
|
||||
case CLEAR_VIEWPORT: {
|
||||
let viewportSpecificData = cloneDeep(state.viewportSpecificData);
|
||||
|
||||
if (action.viewportIndex) {
|
||||
viewportSpecificData[action.viewportIndex] = {};
|
||||
return Object.assign({}, state, { viewportSpecificData });
|
||||
return { ...state, viewportSpecificData };
|
||||
} else {
|
||||
return defaultState;
|
||||
return DEFAULT_STATE;
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
/**
|
||||
* Returns the current application state.
|
||||
*
|
||||
* @return {Object} The current state.
|
||||
*/
|
||||
default: {
|
||||
return state;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -60,7 +60,7 @@ describe('viewports reducer', () => {
|
||||
const setViewportAction = {
|
||||
type: types.SET_VIEWPORT,
|
||||
viewportIndex: viewportToSet,
|
||||
data: {
|
||||
viewportSpecificData: {
|
||||
hello: 'this is that data for the viewport',
|
||||
world: 'that will be set for the viewportIndex',
|
||||
},
|
||||
@@ -69,7 +69,7 @@ describe('viewports reducer', () => {
|
||||
const updatedState = reducer(undefined, setViewportAction);
|
||||
const updatedViewport = updatedState.viewportSpecificData[viewportToSet];
|
||||
|
||||
expect(updatedViewport).toEqual(setViewportAction.data);
|
||||
expect(updatedViewport).toEqual(setViewportAction.viewportSpecificData);
|
||||
});
|
||||
|
||||
it('should handle CLEAR_VIEWPORT', () => {
|
||||
|
||||
@@ -1,9 +1,49 @@
|
||||
import log from './../log.js';
|
||||
|
||||
export default class ServicesManager {
|
||||
constructor() {
|
||||
this.services = {};
|
||||
this.registeredServiceNames = [];
|
||||
}
|
||||
|
||||
register(service) {
|
||||
/**
|
||||
*
|
||||
* @param {Object} service
|
||||
*/
|
||||
registerService(service) {
|
||||
if (!service) {
|
||||
log.warn(
|
||||
'Attempting to register a null/undefined service. Exiting early.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let serviceName = service.name;
|
||||
|
||||
if (!serviceName) {
|
||||
log.warn(`Service name not set. Exiting early.`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.registeredServiceNames.includes(serviceName)) {
|
||||
log.warn(
|
||||
`Extension name ${serviceName} has already been registered. Exiting before duplicating services.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
this.services[service.name] = service;
|
||||
|
||||
// Track service registration
|
||||
this.registeredServiceNames.push(serviceName);
|
||||
}
|
||||
|
||||
/**
|
||||
* An array of services.
|
||||
*
|
||||
* @param {Object[]} services - Array of services
|
||||
*/
|
||||
registerServices(services) {
|
||||
services.forEach(service => this.registerService(service));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import ServicesManager from './ServicesManager.js';
|
||||
import log from '../log.js';
|
||||
|
||||
jest.mock('./../log.js');
|
||||
|
||||
describe('ServicesManager.js', () => {
|
||||
let servicesManager;
|
||||
|
||||
beforeEach(() => {
|
||||
servicesManager = new ServicesManager();
|
||||
log.warn.mockClear();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('registerServices()', () => {
|
||||
it('calls registerService() for each service', () => {
|
||||
servicesManager.registerService = jest.fn();
|
||||
|
||||
const fakeServices = [
|
||||
{ name: 'UINotificationTestService', hide: jest.fn() },
|
||||
{ name: 'UIModalTestService', hide: jest.fn() },
|
||||
];
|
||||
|
||||
servicesManager.registerServices(fakeServices);
|
||||
|
||||
expect(servicesManager.registerService.mock.calls.length).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('registerService()', () => {
|
||||
it('logs a warning if the service is null or undefined', () => {
|
||||
const undefinedService = undefined;
|
||||
const nullService = null;
|
||||
|
||||
servicesManager.registerService(undefinedService);
|
||||
servicesManager.registerService(nullService);
|
||||
|
||||
expect(log.warn.mock.calls.length).toBe(2);
|
||||
});
|
||||
|
||||
it('logs a warning if the service does not have a name', () => {
|
||||
const serviceWithEmptyName = { name: '', hide: jest.fn() };
|
||||
const serviceWithoutName = { hide: jest.fn() };
|
||||
|
||||
servicesManager.registerService(serviceWithEmptyName);
|
||||
servicesManager.registerService(serviceWithoutName);
|
||||
|
||||
expect(log.warn.mock.calls.length).toBe(2);
|
||||
});
|
||||
|
||||
it('tracks which services have been registered', () => {
|
||||
const service = {
|
||||
name: 'UINotificationService',
|
||||
};
|
||||
|
||||
servicesManager.registerService(service);
|
||||
|
||||
expect(servicesManager.registeredServiceNames).toContain(service.name);
|
||||
});
|
||||
|
||||
it('logs a warning if the service has an name that has already been registered', () => {
|
||||
const service = { name: 'UINotificationService' };
|
||||
|
||||
servicesManager.registerService(service);
|
||||
servicesManager.registerService(service);
|
||||
|
||||
expect(log.warn.mock.calls.length).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,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,88 @@
|
||||
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
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.62.2](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.62.1...@ohif/ui@0.62.2) (2019-11-15)
|
||||
|
||||
**Note:** Version bump only for package @ohif/ui
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/ui",
|
||||
"version": "0.62.2",
|
||||
"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();
|
||||
@@ -37,8 +37,8 @@ const SnackbarContainer = () => {
|
||||
|
||||
return (
|
||||
<div key={pos} className={`sb-container sb-${pos}`}>
|
||||
{items[pos].map(item => (
|
||||
<div key={item.id}>{renderItem(item)}</div>
|
||||
{items[pos].map((item, index) => (
|
||||
<div key={item.id + index}>{renderItem(item)}</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -102,14 +102,14 @@ function ImageThumbnail(props) {
|
||||
{shouldRenderToCanvas() ? (
|
||||
<canvas ref={canvasRef} width={width} height={height} />
|
||||
) : (
|
||||
<img
|
||||
className="static-image"
|
||||
src={imageSrc}
|
||||
//width={this.props.width}
|
||||
height={height}
|
||||
alt={''}
|
||||
/>
|
||||
)}
|
||||
<img
|
||||
className="static-image"
|
||||
src={imageSrc}
|
||||
//width={this.props.width}
|
||||
height={height}
|
||||
alt={''}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{loadingOrError}
|
||||
{showStackLoadingProgressBar && (
|
||||
|
||||
@@ -2,22 +2,23 @@
|
||||
|
||||
/* Will edit everything selected including everything between a range of dates */
|
||||
.CalendarDay__selected_span {
|
||||
background: var(--table-text-secondary-color);
|
||||
color: #fff;
|
||||
background: var(--datePicker-picker-background);
|
||||
color: var(--datePicker-picker-text);
|
||||
border-color: #e4e7e7;
|
||||
}
|
||||
|
||||
/* Will edit selected date or the endpoints of a range of dates */
|
||||
.CalendarDay__selected {
|
||||
background: var(--table-text-secondary-color);
|
||||
color: #fff;
|
||||
background: var(--datePicker-picker-background);
|
||||
color: var(--datePicker-picker-text);
|
||||
border-color: #e4e7e7;
|
||||
}
|
||||
|
||||
/* Will edit when hovered over. _span style also has this property */
|
||||
.CalendarDay__selected:hover {
|
||||
background: var(--table-text-secondary-color);
|
||||
color: #fff;
|
||||
.CalendarDay__selected:hover,
|
||||
.CalendarDay__default:hover {
|
||||
background: var(--datePicker-picker-background);
|
||||
color: var(--datePicker-picker-text);
|
||||
border-color: #e4e7e7;
|
||||
}
|
||||
|
||||
@@ -25,8 +26,8 @@
|
||||
is not yet selected. Edits the dates between your mouse and said date */
|
||||
.CalendarDay__hovered_span:hover,
|
||||
.CalendarDay__hovered_span {
|
||||
background: var(--table-text-secondary-color);
|
||||
color: #fff;
|
||||
background: var(--datePicker-picker-background);
|
||||
color: var(--datePicker-picker-text);
|
||||
border-color: #e4e7e7;
|
||||
}
|
||||
|
||||
@@ -42,15 +43,15 @@ is not yet selected. Edits the dates between your mouse and said date */
|
||||
/* Container - visual */
|
||||
.DateRangePickerInput {
|
||||
width: calc(100% - 10px); /* Just use padding? */
|
||||
background-color: var(--input-background-color);
|
||||
border-color: var(--input-background-color);
|
||||
color: var(--input-placeholder-color);
|
||||
background-color: var(--datePicker-input-background);
|
||||
border-color: var(--datePicker-input-background);
|
||||
color: var(--datePicker-picker-text-input);
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.DateRangePickerInput.DateRangePickerInput__withBorder {
|
||||
border-radius: 4px;
|
||||
background-color: var(--input-background-color);
|
||||
background-color: var(--datePicker-input-background);
|
||||
}
|
||||
|
||||
/* Input Container */
|
||||
@@ -58,14 +59,14 @@ is not yet selected. Edits the dates between your mouse and said date */
|
||||
width: 97px;
|
||||
height: 38px;
|
||||
border-radius: 4px;
|
||||
background-color: var(--input-background-color);
|
||||
background-color: var(--datePicker-input-background);
|
||||
}
|
||||
|
||||
/* Actual Input Element */
|
||||
.DateInput > .DateInput_input {
|
||||
border-color: transparent;
|
||||
background-color: transparent;
|
||||
color: var(--input-placeholder-color);
|
||||
color: var(--datePicker-picker-text-input);
|
||||
height: 38px;
|
||||
font-size: 10pt;
|
||||
padding: 0;
|
||||
@@ -80,4 +81,39 @@ is not yet selected. Edits the dates between your mouse and said date */
|
||||
.PresetDateRangePicker_button {
|
||||
margin: 0;
|
||||
padding: 4px 8px;
|
||||
color: #00a699;
|
||||
background: var(--datePicker-picker-text-input);
|
||||
}
|
||||
|
||||
.PresetDateRangePicker_button:hover {
|
||||
background: #00a699;
|
||||
color: var(--datePicker-picker-text-input);
|
||||
}
|
||||
|
||||
.CalendarDay__blocked_out_of_range:hover {
|
||||
background: #fff;
|
||||
cursor: not-allowed;
|
||||
color: var(--datePicker-picker-text-disabled);
|
||||
}
|
||||
|
||||
.DateRangePicker_select {
|
||||
background-color: white;
|
||||
color: var(--datePicker-picker-text);
|
||||
border: 1px solid #e4e7e7;
|
||||
border-radius: 4px;
|
||||
display: inline-block;
|
||||
padding: 3px 20px 3px 10px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
appearance: none;
|
||||
|
||||
background-image: linear-gradient(45deg, transparent 50%, gray 50%),
|
||||
linear-gradient(135deg, gray 50%, transparent 50%);
|
||||
background-position: calc(100% - 11px) 11px, calc(100% - 6px) calc(11px);
|
||||
background-size: 5px 5px, 5px 5px;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.DateRangePicker_select:hover {
|
||||
border-color: #c4c4c4;
|
||||
}
|
||||
@@ -7,39 +7,25 @@ import './CustomDateRangePicker.css';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { DateRangePicker } from 'react-dates';
|
||||
import moment from 'moment';
|
||||
import i18n from '@ohif/i18n';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export default class CustomDateRangePicker extends React.Component {
|
||||
static propTypes = {
|
||||
presets: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
text: PropTypes.string,
|
||||
start: PropTypes.required,
|
||||
end: PropTypes.required,
|
||||
})
|
||||
),
|
||||
};
|
||||
function CustomDateRangePicker(props) {
|
||||
moment.locale(i18n.language); // using i18n in the date picker
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
startDate: this.props.startDate,
|
||||
endDate: this.props.endDate,
|
||||
};
|
||||
const { t } = useTranslation('DatePicker');
|
||||
|
||||
this.renderDatePresets = this.renderDatePresets.bind(this);
|
||||
this.onDatesChange = this.onDatesChange.bind(this);
|
||||
}
|
||||
const {
|
||||
onDatesChange,
|
||||
startDate,
|
||||
endDate,
|
||||
presets,
|
||||
...dateRangePickerProps
|
||||
} = props;
|
||||
|
||||
onDatesChange({ startDate, endDate, preset }) {
|
||||
this.setState({ startDate, endDate });
|
||||
if (this.props.onDatesChange) {
|
||||
this.props.onDatesChange({ startDate, endDate, preset });
|
||||
}
|
||||
}
|
||||
|
||||
renderDatePresets() {
|
||||
const { presets } = this.props;
|
||||
const { startDate, endDate } = this.state;
|
||||
const renderDatePresets = () => {
|
||||
const { presets } = props;
|
||||
|
||||
return (
|
||||
<div className="PresetDateRangePicker_panel">
|
||||
@@ -54,7 +40,7 @@ export default class CustomDateRangePicker extends React.Component {
|
||||
isSelected ? 'PresetDateRangePicker_button__selected' : ''
|
||||
}`}
|
||||
onClick={() =>
|
||||
this.onDatesChange({
|
||||
onDatesChange({
|
||||
startDate: start,
|
||||
endDate: end,
|
||||
preset: true,
|
||||
@@ -67,30 +53,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;
|
||||
Loaded 100 of 133 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user