Compare commits
54
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5d47cc43c1 | ||
|
|
e46a11a01e | ||
|
|
27a950d956 | ||
|
|
03d4c10adc | ||
|
|
7a0c369006 | ||
|
|
8c946a4742 | ||
|
|
30b9e44220 | ||
|
|
380abe2705 | ||
|
|
f4570c96c2 | ||
|
|
1fe951a71e | ||
|
|
ebe1af8d4f | ||
|
|
5cda5159da | ||
|
|
115a3ec0b0 | ||
|
|
ac82a111e7 | ||
|
|
a22fd67e13 | ||
|
|
91f0af058f | ||
|
|
5a1186388d | ||
|
|
2f0f92c095 | ||
|
|
5c686268ab | ||
|
|
974aabc9b2 | ||
|
|
17e3b49b06 | ||
|
|
e3c119489a | ||
|
|
17447baf44 | ||
|
|
daa3ddccdb | ||
|
|
c91b6dddd8 | ||
|
|
92548191e4 | ||
|
|
68fcaef19a | ||
|
|
ef611e4849 | ||
|
|
d128fd6bf0 | ||
|
|
3bdd434488 | ||
|
|
29bac8df7a | ||
|
|
319c67c389 | ||
|
|
b9836f6c71 | ||
|
|
6a6e2d7df3 | ||
|
|
2f8c408ed2 | ||
|
|
27910239e0 | ||
|
|
719bfd02eb | ||
|
|
994818faeb | ||
|
|
f06ef53e83 | ||
|
|
c7008621e4 | ||
|
|
55a580659e | ||
|
|
fd7620ced9 | ||
|
|
cd1cbf0f85 | ||
|
|
08055398de | ||
|
|
f6db7fade6 | ||
|
|
58d2bf1d12 | ||
|
|
87f5eca659 | ||
|
|
922ba8cc42 | ||
|
|
777ff88f3d | ||
|
|
68858db4b6 | ||
|
|
ab106afae3 | ||
|
|
ac69b0a656 | ||
|
|
8edba24eec | ||
|
|
aae232fc3e |
No files matched your search
@@ -367,6 +367,31 @@ workflows:
|
||||
requires:
|
||||
- UNIT_TESTS
|
||||
|
||||
PR_OPTIONAL_VISUAL_TESTS:
|
||||
jobs:
|
||||
- AWAIT_APPROVAL:
|
||||
type: approval
|
||||
# Update hub.docker.org
|
||||
- cypress/run:
|
||||
name: 'Generate Percy Snapshots'
|
||||
executor: cypress/browsers-chrome76
|
||||
pre-steps:
|
||||
- run: 'rm -rf ~/.yarn && npm i -g yarn && yarn -v && yarn global
|
||||
add wait-on' # Use yarn latest
|
||||
yarn: true
|
||||
store_artifacts: false
|
||||
working_directory: platform/viewer
|
||||
build: npx cross-env QUICK_BUILD=true yarn run build
|
||||
# start server --> verify running --> percy + chrome + cypress
|
||||
command: yarn run test:e2e:dist
|
||||
cache-key: 'yarn-packages-{{ checksum "yarn.lock" }}'
|
||||
no-workspace: true # Don't persist workspace
|
||||
post-steps:
|
||||
- store_artifacts:
|
||||
path: platform/viewer/cypress/screenshots
|
||||
requires:
|
||||
- AWAIT_APPROVAL
|
||||
|
||||
PR_OPTIONAL_DOCKER_PUBLISH:
|
||||
jobs:
|
||||
# https://circleci.com/docs/2.0/workflows/#holding-a-workflow-for-a-manual-approval
|
||||
@@ -419,6 +444,28 @@ workflows:
|
||||
filters:
|
||||
branches:
|
||||
only: master
|
||||
# Update base branch snapshots
|
||||
# and record a Cypress dashboard test run
|
||||
- cypress/run:
|
||||
name: 'Generate Percy Snapshots'
|
||||
executor: cypress/browsers-chrome76
|
||||
pre-steps:
|
||||
- run: 'rm -rf ~/.yarn && npm i -g yarn && yarn -v && yarn global
|
||||
add wait-on' # Use yarn latest
|
||||
yarn: true
|
||||
store_artifacts: false
|
||||
working_directory: platform/viewer
|
||||
build: npx cross-env QUICK_BUILD=true yarn run build
|
||||
# start server --> verify running --> percy + chrome + cypress
|
||||
command: yarn run test:e2e:dist
|
||||
cache-key: 'yarn-packages-{{ checksum "yarn.lock" }}'
|
||||
no-workspace: true # Don't persist workspace
|
||||
post-steps:
|
||||
- store_artifacts:
|
||||
path: platform/viewer/cypress/screenshots
|
||||
filters:
|
||||
branches:
|
||||
only: master
|
||||
- DOCKER_MASTER_PUBLISH:
|
||||
requires:
|
||||
- NPM_PUBLISH
|
||||
|
||||
File renamed without changes.
+1
-1
@@ -9,7 +9,7 @@ daysUntilClose: 7
|
||||
exemptLabels:
|
||||
- 'Story :raised_hands:'
|
||||
- 'Bug: Verified :bug:'
|
||||
- 'Task: CI/Tooling 🤖'
|
||||
- 'Task: CI/Tooling :robot:'
|
||||
- 'Task: Docs 📖'
|
||||
- 'Task: Docs :book:'
|
||||
- 'Task: Refactor 🛠'
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
In the interest of fostering an open and welcoming environment, we as
|
||||
contributors and maintainers pledge to making participation in our project and
|
||||
our community a harassment-free experience for everyone, regardless of age, body
|
||||
size, disability, ethnicity, sex characteristics, gender identity and expression,
|
||||
level of experience, education, socio-economic status, nationality, personal
|
||||
appearance, race, religion, or sexual identity and orientation.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to creating a positive environment
|
||||
include:
|
||||
|
||||
* Using welcoming and inclusive language
|
||||
* Being respectful of differing viewpoints and experiences
|
||||
* Gracefully accepting constructive criticism
|
||||
* Focusing on what is best for the community
|
||||
* Showing empathy towards other community members
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
|
||||
* The use of sexualized language or imagery and unwelcome sexual attention or
|
||||
advances
|
||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or electronic
|
||||
address, without explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Our Responsibilities
|
||||
|
||||
Project maintainers are responsible for clarifying the standards of acceptable
|
||||
behavior and are expected to take appropriate and fair corrective action in
|
||||
response to any instances of unacceptable behavior.
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or
|
||||
reject comments, commits, code, wiki edits, issues, and other contributions
|
||||
that are not aligned to this Code of Conduct, or to ban temporarily or
|
||||
permanently any contributor for other behaviors that they deem inappropriate,
|
||||
threatening, offensive, or harmful.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies both within project spaces and in public spaces
|
||||
when an individual is representing the project or its community. Examples of
|
||||
representing a project or community include using an official project e-mail
|
||||
address, posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event. Representation of a project may be
|
||||
further defined and clarified by project maintainers.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported by contacting the project team at danny.ri.brown+OHIFcoc@gmail.com. All
|
||||
complaints will be reviewed and investigated and will result in a response that
|
||||
is deemed necessary and appropriate to the circumstances. The project team is
|
||||
obligated to maintain confidentiality with regard to the reporter of an incident.
|
||||
Further details of specific enforcement policies may be posted separately.
|
||||
|
||||
Project maintainers who do not follow or enforce the Code of Conduct in good
|
||||
faith may face temporary or permanent repercussions as determined by other
|
||||
members of the project's leadership.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
||||
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see
|
||||
https://www.contributor-covenant.org/faq
|
||||
@@ -0,0 +1 @@
|
||||
See our contributing guidelines at [`https://docs.ohif.org`](https://docs.ohif.org/development/contributing.html)
|
||||
@@ -271,8 +271,8 @@ MIT © [OHIF](https://github.com/OHIF)
|
||||
<!-- Badges -->
|
||||
[lerna-image]: https://img.shields.io/badge/maintained%20with-lerna-cc00ff.svg
|
||||
[lerna-url]: https://lerna.js.org/
|
||||
[netlify-image]: https://api.netlify.com/api/v1/badges/a5d369ab-18a6-41c3-bcde-83805205ac7f/deploy-status
|
||||
[netlify-url]: https://app.netlify.com/sites/ohif/deploys
|
||||
[netlify-image]: https://api.netlify.com/api/v1/badges/32708787-c9b0-4634-b50f-7ca41952da77/deploy-status
|
||||
[netlify-url]: https://app.netlify.com/sites/ohif-dev/deploys
|
||||
[all-contributors-image]: https://img.shields.io/badge/all_contributors-0-orange.svg?style=flat-square
|
||||
[circleci-image]: https://circleci.com/gh/OHIF/Viewers.svg?style=svg
|
||||
[circleci-url]: https://circleci.com/gh/OHIF/Viewers
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
- [Toolbar](extensions/modules/toolbar.md)
|
||||
- [Viewport](extensions/modules/viewport.md)
|
||||
- [Contexts](extensions/index.md#contexts)
|
||||
- [ExtensionManager](extensions/index.md#extensionmanager)
|
||||
- [OHIF Maintained](extensions/index.md#maintained-extensions)
|
||||
- [Services](services/index.md)
|
||||
- [Default](services/default/index.md)
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 178 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 137 KiB |
@@ -66,6 +66,52 @@ window.config = {
|
||||
};
|
||||
```
|
||||
|
||||
The configuration can also be written as a JS Function in case you need to inject dependencies like external services:
|
||||
|
||||
```js
|
||||
window.config = ({ servicesManager } = {}) => {
|
||||
const { UIDialogService } = servicesManager.services;
|
||||
return {
|
||||
cornerstoneExtensionConfig: {
|
||||
tools: {
|
||||
ArrowAnnotate: {
|
||||
configuration: {
|
||||
getTextCallback: (callback, eventDetails) => UIDialogService.create({...
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
routerBasename: '/',
|
||||
servers: {
|
||||
dicomWeb: [
|
||||
{
|
||||
name: 'DCM4CHEE',
|
||||
wadoUriRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/wado',
|
||||
qidoRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/rs',
|
||||
wadoRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/rs',
|
||||
qidoSupportsIncludeField: true,
|
||||
imageRendering: 'wadors',
|
||||
thumbnailRendering: 'wadors',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
You can also create a new config file and specify its path relative to the build
|
||||
output's root by setting the `APP_CONFIG` environment variable. You can set the
|
||||
value of this environment variable a few different ways:
|
||||
|
||||
- ~[Add a temporary environment variable in your shell](https://facebook.github.io/create-react-app/docs/adding-custom-environment-variables#adding-temporary-environment-variables-in-your-shell)~
|
||||
- Previous `react-scripts` functionality that we need to duplicate with
|
||||
`dotenv-webpack`
|
||||
- ~[Add environment specific variables in `.env` file(s)](https://facebook.github.io/create-react-app/docs/adding-custom-environment-variables#adding-development-environment-variables-in-env)~
|
||||
- Previous `react-scripts` functionality that we need to duplicate with
|
||||
`dotenv-webpack`
|
||||
- Using the `cross-env` package in an npm script:
|
||||
- `"build": "cross-env APP_CONFIG=config/my-config.js react-scripts build"`
|
||||
|
||||
After updating the configuration, `yarn run build` to generate updated build
|
||||
output.
|
||||
|
||||
|
||||
@@ -24,12 +24,12 @@ include tags. Here's how it works:
|
||||
</ul>
|
||||
|
||||
<ol start="2">
|
||||
<li>Create a JS Object to hold the OHIF Viewer's configuration. Here are some
|
||||
<li>Create a JS Object or Function to hold the OHIF Viewer's configuration. Here are some
|
||||
example values that would allow the viewer to hit our public PACS:</li>
|
||||
</ol>
|
||||
|
||||
```js
|
||||
// Set before importing `ohif-viewer`
|
||||
// Set before importing `ohif-viewer` (JS Object)
|
||||
window.config = {
|
||||
// default: '/'
|
||||
routerBasename: '/',
|
||||
@@ -49,6 +49,9 @@ window.config = {
|
||||
};
|
||||
```
|
||||
|
||||
To learn more about how you can configure the OHIF Viewer, check out our
|
||||
[Configuration Guide](./index.md).
|
||||
|
||||
<ol start="3"><li>
|
||||
Render the viewer in the web page's target <code>div</code>
|
||||
</li></ol>
|
||||
|
||||
@@ -8,12 +8,15 @@
|
||||
- [Modules](#modules)
|
||||
- [Contexts](#contexts)
|
||||
- [Consuming Extensions](#consuming-extensions)
|
||||
- [Extension Manager](#extensionmanager)
|
||||
- [Maintained Extensions](#maintained-extensions)
|
||||
|
||||
## Overview
|
||||
|
||||
We use extensions to help us isolate and package groups of related features.
|
||||
Extensions provide functionality, ui components, and new behaviors.
|
||||
Extensions provide functionality, ui components, and new behaviors. Ideally,
|
||||
they're built in a way that allows them to extend entirely different
|
||||
implementations of the `@ohif/viewer` project.
|
||||
|
||||
<div style="text-align: center;">
|
||||
<a href="/assets/img/extensions-diagram.png">
|
||||
@@ -46,9 +49,8 @@ Practical examples of extensions include:
|
||||
|
||||
### Extension Skeleton
|
||||
|
||||
An extension is a plain JavaScript object has an `id` property, and one or more
|
||||
"getModuleFunctions" and/or lifecycle hooks. You can read more about
|
||||
[lifecycle hooks](#lifecycle-hooks) and [modules](#modules) further down.
|
||||
An extension is a plain JavaScript object that has an `id` property, and one or
|
||||
more [modules](#modules) and/or [lifecycle hooks](#lifecycle-hooks).
|
||||
|
||||
```js
|
||||
// prettier-ignore
|
||||
@@ -78,6 +80,12 @@ You can leverage one or both strategies. Which one(s) you choose depend on your
|
||||
application's requirements. Each [module](#modules) defined by the extension
|
||||
becomes available to the core application via the `ExtensionManager`.
|
||||
|
||||
#### Registering at Runtime
|
||||
|
||||
The `@ohif/viewer` uses a [configuration file](#) at startup. The schema for
|
||||
that file includes an `Extensions` key that supports an array of extensions to
|
||||
register.
|
||||
|
||||
```js
|
||||
// prettier-ignore
|
||||
const config = {
|
||||
@@ -91,19 +99,13 @@ const config = {
|
||||
}
|
||||
```
|
||||
|
||||
#### Registering at Runtime
|
||||
|
||||
The `@ohif/viewer` uses a [configuration file](#) at startup. The schema for
|
||||
that file includes an `Extensions` key that supports an array of extensions to
|
||||
register.
|
||||
|
||||
#### Registering at Build Time
|
||||
|
||||
The `@ohif/viewer` works best when built as a "Progressive Web Application"
|
||||
(PWA). If you know the extensions your application will need, you can specify
|
||||
them at "build time" to leverage advantages afforded to us by modern tooling:
|
||||
|
||||
- Code Splitting
|
||||
- Code Splitting (dynamic imports)
|
||||
- Tree Shaking
|
||||
- Dependency deduplication
|
||||
|
||||
@@ -141,6 +143,8 @@ differently.
|
||||
| [Toolbar](./modules/toolbar.md) | Adds buttons or custom components to the toolbar | Toolbar button, nested buttons, custom |
|
||||
| [Viewport](./modules/viewport.md) | Adds a component responsible for rendering a "DisplaySet" | `<CornerstoneViewport />`, `<DicomPdfViewport />` |
|
||||
|
||||
<figure style="text-align: center; font-style: italic;">Tbl. Module types with abridged descriptions and examples. Each module links to a dedicated documentation page.</figure>
|
||||
|
||||
### Contexts
|
||||
|
||||
The `@ohif/viewer` tracks "active contexts" that extensions can use to scope
|
||||
@@ -153,9 +157,64 @@ An extension module can use these to say "Only show this Toolbar Button if the
|
||||
active viewport is a Cornerstone viewport." This helps us use the appropriate UI
|
||||
and behaviors depending on the current contexts.
|
||||
|
||||
For example, if we have hotkey that "rotates the active viewport", each Viewport
|
||||
module that supports this behavior can add a command with the same name, scoped
|
||||
to the appropriate context. When the `command` is fired, the "active contexts"
|
||||
are used to determine the appropriate implementation of the rotate behavior.
|
||||
|
||||
## Consuming Extensions
|
||||
|
||||
...
|
||||
We consume extensions, via the `ExtensionManager`, in our `@ohif/viewer`
|
||||
project.
|
||||
|
||||
```js
|
||||
const extensionManager = new ExtensionManager({
|
||||
commandsManager,
|
||||
servicesManager,
|
||||
});
|
||||
|
||||
// prettier-ignore
|
||||
extensionManager.registerExtensions([ /** **/ ]);
|
||||
```
|
||||
|
||||
The `@ohif/viewer` project handles data fetching, basic routing, wires up UI
|
||||
services, and is the home to the more bespoke application logic that doesn't
|
||||
make as much sense to make reusable.
|
||||
|
||||
Long-term, replacing the `@ohif/viewer` application and consuming extensions
|
||||
(and the `ExtensionManager`) in your own project is the ideal path for
|
||||
applications requiring a high degree of customization that can't be achieved
|
||||
with current theming, configuration, extension, and services support.
|
||||
|
||||
If you're not sure how to achieve your goals with the extensibility available
|
||||
today, create a GitHub issue!
|
||||
|
||||
### `ExtensionManager`
|
||||
|
||||
The `ExtensionManager` is a class made available to us via the `@ohif/core`
|
||||
project (platform/core). Our application instantiates a single instance of it,
|
||||
and provides a `ServicesManager` and `CommandsManager`.
|
||||
|
||||
```js
|
||||
const commandsManager = new CommandsManager();
|
||||
const servicesManager = new ServicesManager();
|
||||
const extensionManager = new ExtensionManager({
|
||||
commandsManager,
|
||||
servicesManager,
|
||||
});
|
||||
```
|
||||
|
||||
The `ExtensionManager` only has a few public members:
|
||||
|
||||
- `registerExtension` - Registers a single extension
|
||||
- `registerExtensions` - Registers an array of extensions
|
||||
- `modules` - An object containing registered extensions by `MODULE_TYPE`
|
||||
|
||||
During registration, lifecycle hooks and modules have access to
|
||||
`ExtensionManager`'s `ServicesManager` and `CommandsManager`.
|
||||
|
||||
Our `@ohif/viewer` uses the `modules` member to access registered extensions at
|
||||
appropriate places in our application.
|
||||
|
||||
## Maintained Extensions
|
||||
|
||||
|
||||
@@ -1,12 +1,31 @@
|
||||
# Lifecylce Hook: preRegistration
|
||||
|
||||
If an extension defines the `preRegistration` lifecycle hook, it is called
|
||||
before any modules are registered in the `ExtensionManager`.
|
||||
before any modules are registered in the `ExtensionManager`. This hook can be
|
||||
used to:
|
||||
|
||||
- initialize 3rd party libraries
|
||||
- register event listeners
|
||||
- add or call services
|
||||
- add or call commands
|
||||
|
||||
The `preRegistration` hook receives an object containing the
|
||||
`ExtensionManager`'s associated `ServicesManager`, `CommandsManager`, and any
|
||||
`configuration` that was provided with the extension at time of registration.
|
||||
|
||||
_Example `preRegistration` hook implementation_
|
||||
|
||||
```js
|
||||
export default {
|
||||
id: 'MyExampleExtension',
|
||||
|
||||
/**
|
||||
* @param {object} params
|
||||
* @param {object} params.configuration
|
||||
* @param {ServicesManager} params.servicesManager
|
||||
* @param {CommandsManager} params.commandsManager
|
||||
* @returns void
|
||||
*/
|
||||
preRegistration({ servicesManager, commandsManager, configuration }) {
|
||||
console.log('Wiring up important stuff.');
|
||||
|
||||
|
||||
@@ -1,34 +1,158 @@
|
||||
# Module: Commands
|
||||
|
||||
The Commands Module allows us to register one or more commands scoped to
|
||||
specific contexts. Commands can be run by [hotkeys][#], [toolbar buttons][#],
|
||||
and any registered custom react component (like a [viewport][#] or [panel][#]).
|
||||
- [Overview](#overview)
|
||||
- [Command Definitions](#command-definitions)
|
||||
- [Commands Manager](#commands-manager)
|
||||
- [Instantiating](#instatiating)
|
||||
- [Public API](#public-api)
|
||||
- [Contexts](#contexts)
|
||||
|
||||
## Overview
|
||||
|
||||
An extension can register a Commands Module by defining a `getCommandsModule`
|
||||
method. The Commands Module allows us to register one or more commands scoped to
|
||||
specific [contexts](./../index.md#contexts). Commands have several unique
|
||||
characteristics that make them tremendously powerful:
|
||||
|
||||
- Multiple implementations for the same command can be defined
|
||||
- Only the correct command's implementation will be run, dependent on the
|
||||
application's "context"
|
||||
- Commands can be called from extensions, modules, and the consuming application
|
||||
|
||||
Here is a simple example commands module:
|
||||
|
||||
```js
|
||||
{
|
||||
getCommandsModule() {
|
||||
return {
|
||||
actions: {
|
||||
speak: ({ viewports, words }) => {
|
||||
console.log(viewports, words);
|
||||
},
|
||||
},
|
||||
definitions: {
|
||||
rotateViewportCW: {
|
||||
commandFn: actions.rotateViewport,
|
||||
storeContexts: ['viewports'],
|
||||
options: { rotation: 90 }
|
||||
},
|
||||
rotateViewportCCW: {
|
||||
commandFn: actions.rotateViewport,
|
||||
storeContexts: ['viewports'],
|
||||
options: { rotation: -90 },
|
||||
context: 'ACTIVE_VIEWER::CORNERSTONE'
|
||||
},
|
||||
},
|
||||
defaultContext: 'VIEWER'
|
||||
}
|
||||
}
|
||||
export default {
|
||||
id: 'example-commands-module',
|
||||
|
||||
/**
|
||||
* @param {object} params
|
||||
* @param {ServicesManager} params.servicesManager
|
||||
* @param {CommandsManager} params.commandsManager
|
||||
*/
|
||||
getCommandsModule({ servicesManager, commandsManager }) {
|
||||
return {
|
||||
definitions: {
|
||||
sayHello: {
|
||||
commandFn: ({ words }) => {
|
||||
console.log(words);
|
||||
},
|
||||
options: { words: 'Hello!' },
|
||||
},
|
||||
},
|
||||
defaultContext: 'VIEWER',
|
||||
};
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
Each definition returned by the Commands Module is registered to the
|
||||
`ExtensionManager`'s `CommandsManager`.
|
||||
|
||||
## Command Definitions
|
||||
|
||||
The command definition consists of a named command (`myCommandName` below) and a
|
||||
`commandFn`. The command name is used to call the command, and the `commandFn`
|
||||
is the "command" that is actioned.
|
||||
|
||||
```js
|
||||
myCommandName: {
|
||||
commandFn: ({ viewports, other, options }) => { },
|
||||
storeContexts: ['viewports'],
|
||||
options: { words: 'Just kidding! Goodbye!' },
|
||||
context: 'ACTIVE_VIEWPORT::CORNERSTONE',
|
||||
}
|
||||
```
|
||||
|
||||
| Property | Type | Description |
|
||||
| --------------- | ------------------ | --------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `commandFn` | func | The function to call when command is run. Receives `options` and `storeContexts`. |
|
||||
| `storeContexts` | string[] | (optional) Expected state objects to be passed in as props. Located using `getAppState` fn defined at `CommandsManager`'s instatiation. |
|
||||
| `options` | object | (optional) Arguments to pass at the time of calling to the `commandFn` |
|
||||
| `context` | string[] or string | (optional) Overrides the `defaultContext`. Let's us know if command is currently "available" to be run. |
|
||||
|
||||
## Command Behavior
|
||||
|
||||
**I have many similar commands. How can I share their `commandFn` and make it
|
||||
reusable?**
|
||||
|
||||
This is where `storeContexts` and `options` come in. We use these in our
|
||||
`setToolActive` command. `storeContexts` helps us identify our `activeViewport`,
|
||||
and `options` allow us to pass in the name of a tool we would like to set as
|
||||
active.
|
||||
|
||||
**If there are multiple valid commands for the application's active contexts**
|
||||
|
||||
- What happens: all commands are run
|
||||
- When to use: A `clearData` command that cleans up state for multiple
|
||||
extensions
|
||||
|
||||
**If no commands are valid for the application's active contexts**
|
||||
|
||||
- What happens: a warning is printed to the console
|
||||
- When to use: a `hotkey` (like "invert") that doesn't make sense for the
|
||||
current viewport (PDF or HTML)
|
||||
|
||||
## `CommandsManager`
|
||||
|
||||
The `CommandsManager` is a class defined in the `@ohif/core` project. A single
|
||||
instance of it should be defined in the consuming application, and it should be
|
||||
used when constructing the `ExtensionManager`.
|
||||
|
||||
### Instantiating
|
||||
|
||||
When we instantiate the `CommandsManager`, we need to pass it two methods:
|
||||
|
||||
- `getAppState` - Should return the application's state when called
|
||||
- `getActiveContexts` - Should return the application's active contexts when
|
||||
called
|
||||
|
||||
These methods are used internally to help determine which commands are currently
|
||||
valid, and how to provide them with any state they may need at the time they are
|
||||
called.
|
||||
|
||||
```js
|
||||
const commandsManager = new CommandsManager({
|
||||
getAppState,
|
||||
getActiveContexts,
|
||||
});
|
||||
```
|
||||
|
||||
### Public API
|
||||
|
||||
If you would like to run a command in the consuming app or an extension, you can
|
||||
use one of the following methods:
|
||||
|
||||
```js
|
||||
// Returns all commands for a given context
|
||||
commandsManager.getContext('string');
|
||||
|
||||
// Attempts to run a command
|
||||
commandsManager.runCommand('speak', { command: 'hello' });
|
||||
|
||||
// Run command, but override the active contexts
|
||||
commandsManager.runCommand('speak', { command: 'hello' }, ['VIEWER']);
|
||||
```
|
||||
|
||||
The `ExtensionManager` handles registering commands and creating contexts, so
|
||||
most consumer's won't need these methods. If you find yourself using these, ask
|
||||
yourself "why can't I register these commands via an extension?"
|
||||
|
||||
```js
|
||||
// Used by the `ExtensionManager` to register new commands
|
||||
commandsManager.registerCommand('context', 'name', commandDefinition);
|
||||
|
||||
// Creates a new context; clears the context if it already exists
|
||||
commandsManager.createContext('string');
|
||||
```
|
||||
|
||||
### Contexts
|
||||
|
||||
It is up to the consuming application to define what contexts are possible, and
|
||||
which ones are currently active. As extensions depend heavily on these, we will
|
||||
likely publish guidance around creating contexts, and ways to override extension
|
||||
defined contexts in the near future. If you would like to discuss potential
|
||||
changes to how contexts work, please don't hesistate to createa new GitHub
|
||||
issue.
|
||||
|
||||
[Some additional information on Contexts can be found here.](./../index.md#contexts)
|
||||
@@ -1,6 +1,20 @@
|
||||
# Module: Panel
|
||||
|
||||
...
|
||||
An extension can register a Panel Module by defining a `getPanelModule` method.
|
||||
The panel module provides the ability to define `menuOptions` and `components`
|
||||
that can be used by the consuming application. `components` are React Components
|
||||
that can be displayed in the consuming application's "Panel" Component.
|
||||
|
||||

|
||||
|
||||
<center><i>A panel extension example</i></center>
|
||||
|
||||
The `menuOptions`'s `target` key points to a registered `components`'s `id`. A
|
||||
`defaultContext` is applied to all `menuOption`s; however, each `menuOption` can
|
||||
optional provide it's own `context` value.
|
||||
|
||||
The `getPanelModule` receives an object containing the `ExtensionManager`'s
|
||||
associated `ServicesManager` and `CommandsManager`.
|
||||
|
||||
```js
|
||||
import MyComponent from './MyComponent.js';
|
||||
@@ -10,15 +24,24 @@ export default {
|
||||
|
||||
/**
|
||||
* @param {object} params
|
||||
* @param {object} params.servicesManager
|
||||
* @param {ServicesManager} params.servicesManager
|
||||
* @param {CommandsManager} params.commandsManager
|
||||
*/
|
||||
getPanelModule({ servicesManager }) {
|
||||
getPanelModule({ servicesManager, commandsManager }) {
|
||||
return {
|
||||
menuOptions: [
|
||||
{
|
||||
// A suggested icon
|
||||
// Available icons determined by consuming app
|
||||
icon: 'list',
|
||||
// A suggested label
|
||||
label: 'Magic',
|
||||
// 'right' or 'left'
|
||||
from: 'right',
|
||||
// The target component to toggle open/close
|
||||
target: 'target-component-id',
|
||||
// Overrides `defaultContext`, if specified
|
||||
context: ['ACTIVE_VIEWPORT:MAGIC'],
|
||||
},
|
||||
],
|
||||
components: [
|
||||
@@ -27,7 +50,7 @@ export default {
|
||||
component: MyComponent,
|
||||
},
|
||||
],
|
||||
defaultContext: ['VIEWER'],
|
||||
defaultContext: ['ROUTE:VIEWER'],
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,6 +1,37 @@
|
||||
# Module: SOP Class Handler
|
||||
|
||||
...
|
||||
An extension can register a SOP Class Handler Module by defining a
|
||||
`getSopClassHandlerModule` method. The SOP Class Handler is a bit different from
|
||||
the other modules, as it doesn't provide a `1:1` schema for UI or provide it's
|
||||
own components. It instead defines:
|
||||
|
||||
- `sopClassUids`: an array of string SOP Class UIDs that the
|
||||
`getDisplaySetFromSeries` method should be applied to.
|
||||
- `getDisplaySetFromSeries`: a method that maps series and study metadata to a
|
||||
display set
|
||||
|
||||
A `displaySet` has the following shape:
|
||||
|
||||
```js
|
||||
return {
|
||||
plugin: 'html',
|
||||
modality: 'SR',
|
||||
displaySetInstanceUid: 0,
|
||||
wadoRoot: study.getData().wadoRoot,
|
||||
wadoUri: instance.getData().wadouri,
|
||||
sopInstanceUid: instance.getSOPInstanceUID(),
|
||||
seriesInstanceUid: series.getSeriesInstanceUID(),
|
||||
studyInstanceUid: study.getStudyInstanceUID(),
|
||||
authorizationHeaders,
|
||||
};
|
||||
```
|
||||
|
||||
Where the `plugin` key is used to influence the default `ViewportComponent` for
|
||||
rendering the `displaySet`. Additional properties are passed to the
|
||||
`ViewportComponent` and used by the default `StudyBrowser` to render
|
||||
"thumbnails" for each `displaySet`
|
||||
|
||||
## Example SOP Class Handler Module
|
||||
|
||||
```js
|
||||
const SOP_CLASS_UIDS = {
|
||||
@@ -8,29 +39,52 @@ const SOP_CLASS_UIDS = {
|
||||
ENHANCED_SR: '1.2.840.10008.5.1.4.1.1.88.22',
|
||||
};
|
||||
|
||||
const sopClassHandlerModule = {
|
||||
id: 'OHIFDicomHtmlSopClassHandler',
|
||||
sopClassUids: Object.values(SOP_CLASS_UIDS),
|
||||
export default {
|
||||
id: 'example-sop-class-handler-module',
|
||||
|
||||
/**
|
||||
* @param {object} series -
|
||||
* @param {object} study -
|
||||
* @param {object} dicomWebClient -
|
||||
* @param {object} authorizationHeaders -
|
||||
*/
|
||||
getDisplaySetFromSeries(series, study, dicomWebClient, authorizationHeaders) {
|
||||
const instance = series.getFirstInstance();
|
||||
/**
|
||||
* @param {object} params
|
||||
* @param {ServicesManager} params.servicesManager
|
||||
* @param {CommandsManager} params.commandsManager
|
||||
*/
|
||||
getSopClassHandlerModule({ servicesManager, commandsManager }) {
|
||||
return {
|
||||
id: 'OHIFDicomHtmlSopClassHandler',
|
||||
sopClassUids: Object.values(SOP_CLASS_UIDS),
|
||||
|
||||
return {
|
||||
plugin: 'html',
|
||||
displaySetInstanceUid: 0,
|
||||
wadoRoot: study.getData().wadoRoot,
|
||||
wadoUri: instance.getData().wadouri,
|
||||
sopInstanceUid: instance.getSOPInstanceUID(),
|
||||
seriesInstanceUid: series.getSeriesInstanceUID(),
|
||||
studyInstanceUid: study.getStudyInstanceUID(),
|
||||
authorizationHeaders,
|
||||
};
|
||||
},
|
||||
/**
|
||||
* @param {object} series -
|
||||
* @param {object} study -
|
||||
* @param {object} dicomWebClient -
|
||||
* @param {object} authorizationHeaders -
|
||||
*/
|
||||
getDisplaySetFromSeries(series, study, dicomWebClient, authorizationHeaders) {
|
||||
const instance = series.getFirstInstance();
|
||||
|
||||
return {
|
||||
plugin: 'html',
|
||||
displaySetInstanceUid: 0,
|
||||
wadoRoot: study.getData().wadoRoot,
|
||||
wadoUri: instance.getData().wadouri,
|
||||
sopInstanceUid: instance.getSOPInstanceUID(),
|
||||
seriesInstanceUid: series.getSeriesInstanceUID(),
|
||||
studyInstanceUid: study.getStudyInstanceUID(),
|
||||
authorizationHeaders,
|
||||
};
|
||||
},
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
## `@ohif/viewer` usage
|
||||
|
||||
We use the `sopClassHandlerModule`s in three different places:
|
||||
|
||||
- `ViewerLocalFileData.js`
|
||||
- `ViewerRetrieveStudyData.js`
|
||||
- `StandaloneRouting.js`
|
||||
|
||||
Each time, it is used to map study and series data to `displaySets`. It does
|
||||
this by working alongside the `StudyMetadataManager` in `@ohif/core`. That
|
||||
manager has the method `createDisplaySets` that takes an array of
|
||||
`sopClassHandlerModules`.
|
||||
@@ -1,48 +1,130 @@
|
||||
# Module: Toolbar
|
||||
|
||||
An extension can register a Toolbar Module by providing a `getToolbarModule()`
|
||||
method that returns a React Component. The component does not receive any props.
|
||||
If you want to modify or react to state, you will need to connect to the redux
|
||||
store. The given toolbar must determine its set of elements and the context of
|
||||
them. The set of elements will be listed on toolbar `definitions`.
|
||||
An extension can register a Toolbar Module by defining a `getToolbarModule`
|
||||
method. This module is commonly used to define:
|
||||
|
||||

|
||||
- [Toolbar buttons](#button-definitions)
|
||||
- [Nested toolbar menus](#nested-toolbar-menus)
|
||||
- [Custom components](#custom-components)
|
||||
|
||||
<center><i>A toolbar extension example</i></center>
|
||||

|
||||
|
||||
Toolbar components are rendered in the `ToolbarRow` component.
|
||||
<center><i>Example toolbar button using the Dialog Service to show CINE controls.</i></center>
|
||||
|
||||
For a complete example implementation,
|
||||
[check out the OHIFCornerstoneViewport's Toolbar Module](https://github.com/OHIF/Viewers/blob/master/extensions/cornerstone/src/toolbarModule.js).
|
||||
## Example Toolbar Module
|
||||
|
||||
## Toolbar Custom Component
|
||||
|
||||
Toolbar elements can define its own custom react component to be consumed when
|
||||
rendering it. So far, it accepts `Functional` and `Class` Components. For that,
|
||||
you just need to expose your `CustomToolbarComponent` as the value of key
|
||||
`CustomComponent`. In case the property `CustomComponent` is not present, a
|
||||
default toolbar component will be used to render it. See bellow
|
||||
The Toolbar Module should return an array of `definitions` and a
|
||||
`defaultContext`. There are currently a few different variations of definitions,
|
||||
each one is detailed further down.
|
||||
|
||||
```js
|
||||
definitions: [
|
||||
...
|
||||
export default {
|
||||
id: 'example-toolbar-module',
|
||||
|
||||
/**
|
||||
* @param {object} params
|
||||
* @param {ServicesManager} params.servicesManager
|
||||
* @param {CommandsManager} params.commandsManager
|
||||
*/
|
||||
getToolbarModule({ servicesManager, commandsManager }) {
|
||||
return {
|
||||
definitions: [
|
||||
/* Array of definitions */
|
||||
],
|
||||
defaultContext: ['ROUTE:VIEWER'],
|
||||
};
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
## Button Definitions
|
||||
|
||||
The simplest definition has the following properties:
|
||||
|
||||
```js
|
||||
{
|
||||
id: 'StackScroll',
|
||||
label: 'Stack Scroll',
|
||||
icon: 'bars',
|
||||
type: 'setToolActive',
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: { toolName: 'StackScroll' },
|
||||
},
|
||||
```
|
||||
|
||||
| property | description | values |
|
||||
| ---------------- | ----------------------------------------------------------------- | ----------------------------------------- |
|
||||
| `id` | Unique string identifier for the definition | \* |
|
||||
| `label` | User/display friendly to show in UI | \* |
|
||||
| `icon` | A string name for an icon supported by the consuming application. | \* |
|
||||
| `type` | Used to determine the button's component and behavior | `"setToolActive"`, `"command"` |
|
||||
| `commandName` | (optional) The command to run when the button is used. | Any command registed by a `CommandModule` |
|
||||
| `commandOptions` | (optional) Options to pass the target `commandName` | \* |
|
||||
| `context` | (optional) Overrides module's `defaultContext` | Array of string context names |
|
||||
|
||||
Where a button with a `type` of `setToolActive` has an "active" styling applied
|
||||
when clicked; removing the active styling from all other buttons.
|
||||
|
||||
## Nested Toolbar Menus
|
||||
|
||||
You can indicate that buttons should be grouped and nested in a submenu by
|
||||
including `buttons` property in a definition:
|
||||
|
||||
```js
|
||||
{
|
||||
id: 'More',
|
||||
label: 'More',
|
||||
icon: 'ellipse-circle',
|
||||
buttons: [
|
||||
{
|
||||
id: 'Custom',
|
||||
label: 'Custom',
|
||||
icon: 'custom-icon',
|
||||
CustomComponent: CustomToolbarComponent,
|
||||
}
|
||||
...
|
||||
]
|
||||
id: 'cstInvert',
|
||||
label: 'Invert',
|
||||
icon: 'circle',
|
||||
type: 'command',
|
||||
commandName: 'invertViewport',
|
||||
},
|
||||
],
|
||||
},
|
||||
```
|
||||
|
||||

|
||||
|
||||
<center><i>Example toolbar button demonstrating nested buttons.</i></center>
|
||||
|
||||
## Custom Components
|
||||
|
||||
The Toolbar Modules supports rendering custom components in place of the
|
||||
application's default. In place of the `type`, `commandName`, and
|
||||
`commandOptions` properties, we instead specify a `CustomComponent`.
|
||||
|
||||
```js
|
||||
{
|
||||
id: 'Custom',
|
||||
label: 'Custom',
|
||||
icon: 'custom-icon',
|
||||
CustomComponent: CustomToolbarComponent,
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
`CustomComponent` components will receive the following props:
|
||||
The `CustomComponent` components will receive the following props:
|
||||
|
||||
- parentContext: parent context. (In most of the cases it will be a ToolbarRow
|
||||
instance)
|
||||
- toolbarClickCallback: callback method when clicking on toolbar
|
||||
- button: its own definition object
|
||||
- key: react key prop
|
||||
- activeButtons: list of active elements
|
||||
- isActive: if current
|
||||
```html
|
||||
<CustomComponent
|
||||
parentContext="{parentContext}"
|
||||
toolbarClickCallback="{_handleToolbarButtonClick.bind(this)}"
|
||||
button="{button}"
|
||||
key="{button.id}"
|
||||
activeButtons="{activeButtonsIds}"
|
||||
isActive="{isActive}"
|
||||
/>
|
||||
```
|
||||
|
||||
| Property | Type | Description |
|
||||
| ---------------------- | -------- | ------------------------------- |
|
||||
| `activeButtons` | string[] | list of active buttons |
|
||||
| `button` | object | its own definition object |
|
||||
| `key` | string | React key prop |
|
||||
| `isActive` | boolean | If current button is active |
|
||||
| `parentContext` | ? | The parent component's context? |
|
||||
| `toolbarClickCallback` | func | Callback method for clicks |
|
||||
@@ -1,19 +1,39 @@
|
||||
# Module: Viewport
|
||||
|
||||
An extension can register a Viewport Module by providing a `getViewportModule()`
|
||||
method that returns a React Component. The React component will receive the
|
||||
following props:
|
||||
An extension can register a Viewport Module by defining a `getViewportModule`
|
||||
method that returns a React component. Currently, we use viewport components to
|
||||
add support for:
|
||||
|
||||
```js
|
||||
children: PropTypes.arrayOf(PropTypes.element)
|
||||
studies: PropTypes.object,
|
||||
displaySet: PropTypes.object,
|
||||
viewportData: PropTypes.object, // { studies, displaySet }
|
||||
viewportIndex: PropTypes.number,
|
||||
children: PropTypes.node,
|
||||
customProps: PropTypes.object
|
||||
- 2D Medical Image Viewing (cornerstone ext.)
|
||||
- Structured Reports as HTML (dicom html ext.)
|
||||
- Encapsulated PDFs as PDFs (dicom pdf ext.)
|
||||
- Whole Slide Microscopy Viewing (whole slide ext.)
|
||||
- etc.
|
||||
|
||||
The general pattern is, the [`sopClassHandlerModule`](#) helps us determine
|
||||
which Viewport Component a set of `sopClassUIDs` should default to. The Viewport
|
||||
Component receives props containing a display set it should know how to render.
|
||||
|
||||
## Viewport Component Props
|
||||
|
||||
Each `ViewportComponent` will receive the following props:
|
||||
|
||||
```html
|
||||
<viewportComponent
|
||||
viewportData="{viewportData}"
|
||||
viewportIndex="{viewportIndex}"
|
||||
children="{[children]}"
|
||||
/>
|
||||
```
|
||||
|
||||
| Property | Type | Description |
|
||||
| --------------- | --------------- | --------------------------------- |
|
||||
| `children` | React.element[] | |
|
||||
| `viewportData` | object | `viewportSpecificData` (probably) |
|
||||
| `viewportIndex` | number | |
|
||||
|
||||
### `@ohif/viewer`
|
||||
|
||||
Viewport components are managed by the `ViewportGrid` Component. Which Viewport
|
||||
component is used depends on:
|
||||
|
||||
@@ -23,7 +43,4 @@ component is used depends on:
|
||||
|
||||

|
||||
|
||||
<center><i>An example of three Viewports</i></center>
|
||||
|
||||
For a complete example implementation,
|
||||
[check out the OHIFCornerstoneViewport](https://github.com/OHIF/Viewers/blob/master/extensions/cornerstone/src/OHIFCornerstoneViewport.js).
|
||||
<center><i>An example of three cornerstone Viewports</i></center>
|
||||
@@ -43,7 +43,7 @@ translate to in practice?
|
||||
|
||||
```js
|
||||
// In the application
|
||||
const UINotificationService = createUINotificationService();
|
||||
import UINotificationService from '@ohif/core';
|
||||
const servicesManager = new ServicesManager();
|
||||
|
||||
servicesManager.registerService(UINotificationService);
|
||||
|
||||
@@ -12,8 +12,9 @@ export default {
|
||||
*/
|
||||
|
||||
preRegistration({
|
||||
servicesManager,
|
||||
configuration: extensionConfiguration,
|
||||
servicesManager = {},
|
||||
commandsManager = {},
|
||||
configuration = {},
|
||||
}) {},
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,6 +3,47 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [2.1.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.0.2...@ohif/extension-cornerstone@2.1.0) (2019-12-11)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* 🎸 DICOM SR STOW on MeasurementAPI ([#954](https://github.com/OHIF/Viewers/issues/954)) ([ebe1af8](https://github.com/OHIF/Viewers/commit/ebe1af8d4f75d2483eba869655906d7829bd9666)), closes [#758](https://github.com/OHIF/Viewers/issues/758)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.0.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.0.1...@ohif/extension-cornerstone@2.0.2) (2019-12-11)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.0.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.0.0...@ohif/extension-cornerstone@2.0.1) (2019-12-09)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [2.0.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@1.7.2...@ohif/extension-cornerstone@2.0.0) (2019-12-09)
|
||||
|
||||
|
||||
* feat!: Ability to configure cornerstone tools via extension configuration (#1229) ([55a5806](https://github.com/OHIF/Viewers/commit/55a580659ecb74ca6433461d8f9a05c2a2b69533)), closes [#1229](https://github.com/OHIF/Viewers/issues/1229)
|
||||
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* modifies the exposed react <App /> components props. The contract for providing configuration for the app has changed. Please reference updated documentation for guidance.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.7.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@1.7.1...@ohif/extension-cornerstone@1.7.2) (2019-12-02)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone
|
||||
|
||||
@@ -67,6 +67,24 @@ Our Viewport wraps [cornerstonejs/react-cornerstone-viewport][react-viewport]
|
||||
and is connected the redux store. This module is the most prone to change as we
|
||||
hammer out our Viewport interface.
|
||||
|
||||
## Tool Configuration
|
||||
|
||||
Tools can be configured through extension configuration using the tools key:
|
||||
|
||||
```js
|
||||
...
|
||||
cornerstoneExtensionConfig: {
|
||||
tools: {
|
||||
ArrowAnnotate: {
|
||||
configuration: {
|
||||
getTextCallback: (callback, eventDetails) => callback(prompt('Enter your custom annotation')),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
...
|
||||
```
|
||||
|
||||
## Resources
|
||||
|
||||
### Repositories
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/extension-cornerstone",
|
||||
"version": "1.7.2",
|
||||
"version": "2.1.0",
|
||||
"description": "OHIF extension for Cornerstone",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
@@ -34,7 +34,7 @@
|
||||
"cornerstone-math": "^0.1.8",
|
||||
"cornerstone-tools": "^4.8.0",
|
||||
"cornerstone-wado-image-loader": "^3.0.0",
|
||||
"dcmjs": "^0.6.1",
|
||||
"dcmjs": "^0.8.2",
|
||||
"dicom-parser": "^1.8.3",
|
||||
"hammerjs": "^2.0.8",
|
||||
"prop-types": "^15.6.2",
|
||||
@@ -47,6 +47,7 @@
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.5.5",
|
||||
"classnames": "^2.2.6",
|
||||
"lodash.merge": "^4.6.2",
|
||||
"lodash.throttle": "^4.1.1",
|
||||
"query-string": "^6.8.3",
|
||||
"react-cornerstone-viewport": "2.x.x"
|
||||
|
||||
@@ -248,10 +248,13 @@ class OHIFCornerstoneViewport extends Component {
|
||||
// TODO: Does it make more sense to use Context?
|
||||
if (this.props.children && this.props.children.length) {
|
||||
childrenWithProps = this.props.children.map((child, index) => {
|
||||
return React.cloneElement(child, {
|
||||
viewportIndex: this.props.viewportIndex,
|
||||
key: index,
|
||||
});
|
||||
return (
|
||||
child &&
|
||||
React.cloneElement(child, {
|
||||
viewportIndex: this.props.viewportIndex,
|
||||
key: index,
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -148,18 +148,118 @@ const commandsModule = ({ servicesManager }) => {
|
||||
showDownloadViewportModal: ({ title, viewports }) => {
|
||||
const activeViewportIndex = viewports.activeViewportIndex;
|
||||
const { UIModalService } = servicesManager.services;
|
||||
UIModalService.show({
|
||||
content: CornerstoneViewportDownloadForm,
|
||||
title,
|
||||
contentProps: {
|
||||
activeViewportIndex,
|
||||
onClose: UIModalService.hide,
|
||||
},
|
||||
if (UIModalService) {
|
||||
UIModalService.show({
|
||||
content: CornerstoneViewportDownloadForm,
|
||||
title,
|
||||
contentProps: {
|
||||
activeViewportIndex,
|
||||
onClose: UIModalService.hide,
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
updateTableWithNewMeasurementData({
|
||||
toolType,
|
||||
measurementNumber,
|
||||
location,
|
||||
description,
|
||||
}) {
|
||||
// Update all measurements by measurement number
|
||||
const measurementApi = OHIF.measurements.MeasurementApi.Instance;
|
||||
const measurements = measurementApi.tools[toolType].filter(
|
||||
m => m.measurementNumber === measurementNumber
|
||||
);
|
||||
|
||||
measurements.forEach(measurement => {
|
||||
measurement.location = location;
|
||||
measurement.description = description;
|
||||
|
||||
measurementApi.updateMeasurement(measurement.toolType, measurement);
|
||||
});
|
||||
|
||||
measurementApi.syncMeasurementsAndToolData();
|
||||
|
||||
// Update images in all active viewports
|
||||
cornerstone.getEnabledElements().forEach(enabledElement => {
|
||||
cornerstone.updateImage(enabledElement.element);
|
||||
});
|
||||
},
|
||||
getNearbyToolData({ element, canvasCoordinates, availableToolTypes }) {
|
||||
const nearbyTool = {};
|
||||
let pointNearTool = false;
|
||||
|
||||
availableToolTypes.forEach(toolType => {
|
||||
const elementToolData = cornerstoneTools.getToolState(
|
||||
element,
|
||||
toolType
|
||||
);
|
||||
|
||||
if (!elementToolData) {
|
||||
return;
|
||||
}
|
||||
|
||||
elementToolData.data.forEach((toolData, index) => {
|
||||
let elementToolInstance = cornerstoneTools.getToolForElement(
|
||||
element,
|
||||
toolType
|
||||
);
|
||||
|
||||
if (!elementToolInstance) {
|
||||
elementToolInstance = cornerstoneTools.getToolForElement(
|
||||
element,
|
||||
`${toolType}Tool`
|
||||
);
|
||||
}
|
||||
|
||||
if (!elementToolInstance) {
|
||||
console.warn('Tool not found.');
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (
|
||||
elementToolInstance.pointNearTool(
|
||||
element,
|
||||
toolData,
|
||||
canvasCoordinates
|
||||
)
|
||||
) {
|
||||
pointNearTool = true;
|
||||
nearbyTool.tool = toolData;
|
||||
nearbyTool.index = index;
|
||||
nearbyTool.toolType = toolType;
|
||||
}
|
||||
});
|
||||
|
||||
if (pointNearTool) {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
return pointNearTool ? nearbyTool : undefined;
|
||||
},
|
||||
removeToolState: ({ element, toolType, tool }) => {
|
||||
cornerstoneTools.removeToolState(element, toolType, tool);
|
||||
cornerstone.updateImage(element);
|
||||
},
|
||||
};
|
||||
|
||||
const definitions = {
|
||||
getNearbyToolData: {
|
||||
commandFn: actions.getNearbyToolData,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
removeToolState: {
|
||||
commandFn: actions.removeToolState,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
updateTableWithNewMeasurementData: {
|
||||
commandFn: actions.updateTableWithNewMeasurementData,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
showDownloadViewportModal: {
|
||||
commandFn: actions.showDownloadViewportModal,
|
||||
storeContexts: ['viewports'],
|
||||
|
||||
@@ -4,6 +4,7 @@ import csTools from 'cornerstone-tools';
|
||||
import initCornerstoneTools from './initCornerstoneTools.js';
|
||||
import queryString from 'query-string';
|
||||
import { SimpleDialog } from '@ohif/ui';
|
||||
import merge from 'lodash.merge';
|
||||
|
||||
function fallbackMetaDataProvider(type, imageId) {
|
||||
if (!imageId.includes('wado?requestType=WADO')) {
|
||||
@@ -26,30 +27,33 @@ cornerstone.metaData.addProvider(fallbackMetaDataProvider, -1);
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {object} configuration
|
||||
* @param {Object} servicesManager
|
||||
* @param {Object} configuration
|
||||
* @param {Object|Array} configuration.csToolsConfig
|
||||
*/
|
||||
export default function init({ servicesManager, configuration = {} }) {
|
||||
const { UIDialogService } = servicesManager.services;
|
||||
export default function init({ servicesManager, configuration }) {
|
||||
const callInputDialog = (data, event, callback) => {
|
||||
let dialogId = UIDialogService.create({
|
||||
content: SimpleDialog.InputDialog,
|
||||
defaultPosition: {
|
||||
x: (event && event.currentPoints.canvas.x) || 0,
|
||||
y: (event && event.currentPoints.canvas.y) || 0,
|
||||
},
|
||||
showOverlay: true,
|
||||
contentProps: {
|
||||
title: 'Enter your annotation',
|
||||
label: 'New label',
|
||||
measurementData: data ? { description: data.text } : {},
|
||||
onClose: () => UIDialogService.dismiss({ id: dialogId }),
|
||||
onSubmit: value => {
|
||||
callback(value);
|
||||
UIDialogService.dismiss({ id: dialogId });
|
||||
const { UIDialogService } = servicesManager.services;
|
||||
|
||||
if (UIDialogService) {
|
||||
let dialogId = UIDialogService.create({
|
||||
centralize: true,
|
||||
isDraggable: false,
|
||||
content: SimpleDialog.InputDialog,
|
||||
useLastPosition: false,
|
||||
showOverlay: true,
|
||||
contentProps: {
|
||||
title: 'Enter your annotation',
|
||||
label: 'New label',
|
||||
measurementData: data ? { description: data.text } : {},
|
||||
onClose: () => UIDialogService.dismiss({ id: dialogId }),
|
||||
onSubmit: value => {
|
||||
callback(value);
|
||||
UIDialogService.dismiss({ id: dialogId });
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const { csToolsConfig } = configuration;
|
||||
@@ -73,62 +77,62 @@ export default function init({ servicesManager, configuration = {} }) {
|
||||
initCornerstoneTools(defaultCsToolsConfig);
|
||||
|
||||
// ~~ Toooools 🙌
|
||||
const {
|
||||
PanTool,
|
||||
ZoomTool,
|
||||
WwwcTool,
|
||||
MagnifyTool,
|
||||
StackScrollTool,
|
||||
StackScrollMouseWheelTool,
|
||||
// Touch
|
||||
PanMultiTouchTool,
|
||||
ZoomTouchPinchTool,
|
||||
// Annotations
|
||||
EraserTool,
|
||||
BidirectionalTool,
|
||||
LengthTool,
|
||||
AngleTool,
|
||||
FreehandRoiTool,
|
||||
EllipticalRoiTool,
|
||||
DragProbeTool,
|
||||
RectangleRoiTool,
|
||||
// Segmentation
|
||||
BrushTool,
|
||||
} = csTools;
|
||||
const tools = [
|
||||
PanTool,
|
||||
ZoomTool,
|
||||
WwwcTool,
|
||||
MagnifyTool,
|
||||
StackScrollTool,
|
||||
StackScrollMouseWheelTool,
|
||||
csTools.PanTool,
|
||||
csTools.ZoomTool,
|
||||
csTools.WwwcTool,
|
||||
csTools.MagnifyTool,
|
||||
csTools.StackScrollTool,
|
||||
csTools.StackScrollMouseWheelTool,
|
||||
// Touch
|
||||
PanMultiTouchTool,
|
||||
ZoomTouchPinchTool,
|
||||
csTools.PanMultiTouchTool,
|
||||
csTools.ZoomTouchPinchTool,
|
||||
// Annotations
|
||||
EraserTool,
|
||||
BidirectionalTool,
|
||||
LengthTool,
|
||||
AngleTool,
|
||||
FreehandRoiTool,
|
||||
EllipticalRoiTool,
|
||||
DragProbeTool,
|
||||
RectangleRoiTool,
|
||||
csTools.ArrowAnnotateTool,
|
||||
csTools.EraserTool,
|
||||
csTools.BidirectionalTool,
|
||||
csTools.LengthTool,
|
||||
csTools.AngleTool,
|
||||
csTools.FreehandRoiTool,
|
||||
csTools.EllipticalRoiTool,
|
||||
csTools.DragProbeTool,
|
||||
csTools.RectangleRoiTool,
|
||||
// Segmentation
|
||||
BrushTool,
|
||||
csTools.BrushTool,
|
||||
];
|
||||
|
||||
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),
|
||||
/* Add extension tools configuration here. */
|
||||
const internalToolsConfig = {
|
||||
ArrowAnnotate: {
|
||||
configuration: {
|
||||
getTextCallback: (callback, eventDetails) =>
|
||||
callInputDialog(null, eventDetails, callback),
|
||||
changeTextCallback: (data, eventDetails, callback) =>
|
||||
callInputDialog(data, eventDetails, callback),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/* Add tools with its custom props through extension configuration. */
|
||||
tools.forEach(tool => {
|
||||
const toolName = tool.name.replace('Tool', '');
|
||||
const externalToolsConfig = configuration.tools || {};
|
||||
const externalToolProps = externalToolsConfig[toolName] || {};
|
||||
const internalToolProps = internalToolsConfig[toolName] || {};
|
||||
const props = merge(internalToolProps, externalToolProps);
|
||||
csTools.addTool(tool, props);
|
||||
});
|
||||
|
||||
// TODO -> We need a better way to do this with maybe global tool state setting all tools passive.
|
||||
const BaseAnnotationTool = csTools.importInternal('base/BaseAnnotationTool');
|
||||
tools.forEach(tool => {
|
||||
if (tool.prototype instanceof BaseAnnotationTool) {
|
||||
// BaseAnnotationTool would likely come from csTools lib exports
|
||||
const toolName = new tool().name;
|
||||
csTools.setToolPassive(toolName); // there may be a better place to determine name; may not be on uninstantiated class
|
||||
}
|
||||
});
|
||||
|
||||
csTools.setToolActive('Pan', { mouseButtonMask: 4 });
|
||||
csTools.setToolActive('Zoom', { mouseButtonMask: 2 });
|
||||
csTools.setToolActive('Wwwc', { mouseButtonMask: 1 });
|
||||
|
||||
@@ -3,6 +3,17 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [1.1.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-html@1.0.2...@ohif/extension-dicom-html@1.1.0) (2019-12-11)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* 🎸 DICOM SR STOW on MeasurementAPI ([#954](https://github.com/OHIF/Viewers/issues/954)) ([ebe1af8](https://github.com/OHIF/Viewers/commit/ebe1af8d4f75d2483eba869655906d7829bd9666)), closes [#758](https://github.com/OHIF/Viewers/issues/758)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.0.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-html@1.0.1...@ohif/extension-dicom-html@1.0.2) (2019-12-02)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-html
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/extension-dicom-html",
|
||||
"version": "1.0.2",
|
||||
"version": "1.1.0",
|
||||
"description": "OHIF extension for rendering structured reports to HTML",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
@@ -29,7 +29,7 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@ohif/core": "^0.50.0",
|
||||
"dcmjs": "^0.6.1",
|
||||
"dcmjs": "^0.8.2",
|
||||
"prop-types": "^15.6.2",
|
||||
"react": "^16.8.6",
|
||||
"react-dom": "^16.8.6"
|
||||
|
||||
@@ -3,6 +3,63 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.1.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.1.1...@ohif/extension-vtk@1.1.2) (2019-12-12)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.1.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.1.0...@ohif/extension-vtk@1.1.1) (2019-12-11)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [1.1.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.0.2...@ohif/extension-vtk@1.1.0) (2019-12-11)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* 🎸 DICOM SR STOW on MeasurementAPI ([#954](https://github.com/OHIF/Viewers/issues/954)) ([ebe1af8](https://github.com/OHIF/Viewers/commit/ebe1af8d4f75d2483eba869655906d7829bd9666)), closes [#758](https://github.com/OHIF/Viewers/issues/758)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.0.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.0.1...@ohif/extension-vtk@1.0.2) (2019-12-11)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.0.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.0.0...@ohif/extension-vtk@1.0.1) (2019-12-09)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [1.0.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.54.6...@ohif/extension-vtk@1.0.0) (2019-12-09)
|
||||
|
||||
|
||||
* feat!: Ability to configure cornerstone tools via extension configuration (#1229) ([55a5806](https://github.com/OHIF/Viewers/commit/55a580659ecb74ca6433461d8f9a05c2a2b69533)), closes [#1229](https://github.com/OHIF/Viewers/issues/1229)
|
||||
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* modifies the exposed react <App /> components props. The contract for providing configuration for the app has changed. Please reference updated documentation for guidance.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.54.6](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.54.5...@ohif/extension-vtk@0.54.6) (2019-12-07)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/extension-vtk",
|
||||
"version": "0.54.6",
|
||||
"version": "1.1.2",
|
||||
"description": "OHIF extension for VTK.js",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
@@ -34,7 +34,7 @@
|
||||
"@ohif/ui": "^0.50.0",
|
||||
"cornerstone-core": "^2.2.8",
|
||||
"cornerstone-wado-image-loader": "^3.0.0",
|
||||
"dcmjs": "^0.6.1",
|
||||
"dcmjs": "^0.8.2",
|
||||
"dicom-parser": "^1.8.3",
|
||||
"i18next": "^17.0.3",
|
||||
"i18next-browser-languagedetector": "^3.0.1",
|
||||
@@ -52,11 +52,10 @@
|
||||
"react-vtkjs-viewport": "^0.3.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ohif/core": "^1.13.3",
|
||||
"@ohif/ui": "^0.65.4",
|
||||
"@ohif/core": "^2.1.0",
|
||||
"@ohif/ui": "^1.1.2",
|
||||
"cornerstone-tools": "^4.8.0",
|
||||
"cornerstone-wado-image-loader": "^3.0.0",
|
||||
"dcmjs": "^0.6.1",
|
||||
"dicom-parser": "^1.8.3",
|
||||
"gh-pages": "^2.0.1",
|
||||
"i18next": "^17.0.3",
|
||||
|
||||
@@ -358,10 +358,13 @@ class OHIFVTKViewport extends Component {
|
||||
// TODO: Does it make more sense to use Context?
|
||||
if (this.props.children && this.props.children.length) {
|
||||
childrenWithProps = this.props.children.map((child, index) => {
|
||||
return React.cloneElement(child, {
|
||||
viewportIndex: this.props.viewportIndex,
|
||||
key: index,
|
||||
});
|
||||
return (
|
||||
child &&
|
||||
React.cloneElement(child, {
|
||||
viewportIndex: this.props.viewportIndex,
|
||||
key: index,
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,47 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [2.1.0](https://github.com/OHIF/Viewers/compare/@ohif/core@2.0.2...@ohif/core@2.1.0) (2019-12-11)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* 🎸 DICOM SR STOW on MeasurementAPI ([#954](https://github.com/OHIF/Viewers/issues/954)) ([ebe1af8](https://github.com/OHIF/Viewers/commit/ebe1af8d4f75d2483eba869655906d7829bd9666)), closes [#758](https://github.com/OHIF/Viewers/issues/758)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.0.2](https://github.com/OHIF/Viewers/compare/@ohif/core@2.0.1...@ohif/core@2.0.2) (2019-12-11)
|
||||
|
||||
**Note:** Version bump only for package @ohif/core
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.0.1](https://github.com/OHIF/Viewers/compare/@ohif/core@2.0.0...@ohif/core@2.0.1) (2019-12-09)
|
||||
|
||||
**Note:** Version bump only for package @ohif/core
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [2.0.0](https://github.com/OHIF/Viewers/compare/@ohif/core@1.13.3...@ohif/core@2.0.0) (2019-12-09)
|
||||
|
||||
|
||||
* feat!: Ability to configure cornerstone tools via extension configuration (#1229) ([55a5806](https://github.com/OHIF/Viewers/commit/55a580659ecb74ca6433461d8f9a05c2a2b69533)), closes [#1229](https://github.com/OHIF/Viewers/issues/1229)
|
||||
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* modifies the exposed react <App /> components props. The contract for providing configuration for the app has changed. Please reference updated documentation for guidance.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.13.3](https://github.com/OHIF/Viewers/compare/@ohif/core@1.13.2...@ohif/core@1.13.3) (2019-12-06)
|
||||
|
||||
**Note:** Version bump only for package @ohif/core
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/core",
|
||||
"version": "1.13.3",
|
||||
"version": "2.1.0",
|
||||
"description": "Generic business logic for web-based medical imaging applications",
|
||||
"author": "OHIF Core Team",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -59,7 +59,7 @@ export class CommandsManager {
|
||||
*
|
||||
* @method
|
||||
* @param {string} contextName - Namespace for commands
|
||||
* @returs {Object} - the matched context
|
||||
* @returns {Object} - the matched context
|
||||
*/
|
||||
getContext(contextName) {
|
||||
const context = this.contexts[contextName];
|
||||
|
||||
@@ -26,7 +26,7 @@ export default class ExtensionManager {
|
||||
const hasConfiguration = Array.isArray(extension);
|
||||
|
||||
if (hasConfiguration) {
|
||||
const [ohifExtension, configuration] = extensions;
|
||||
const [ohifExtension, configuration] = extension;
|
||||
this.registerExtension(ohifExtension, configuration);
|
||||
} else {
|
||||
this.registerExtension(extension);
|
||||
|
||||
@@ -37,6 +37,24 @@ describe('ExtensionManager.js', () => {
|
||||
// Assert
|
||||
expect(extensionManager.registerExtension.mock.calls.length).toBe(3);
|
||||
});
|
||||
|
||||
it('calls registerExtension() for each extension passing its configuration if tuple', () => {
|
||||
const fakeConfiguration = { testing: true };
|
||||
extensionManager.registerExtension = jest.fn();
|
||||
|
||||
// SUT
|
||||
const fakeExtensions = [
|
||||
{ one: '1' },
|
||||
[{ two: '2' }, fakeConfiguration],
|
||||
{ three: '3 ' },
|
||||
];
|
||||
extensionManager.registerExtensions(fakeExtensions);
|
||||
|
||||
// Assert
|
||||
expect(extensionManager.registerExtension.mock.calls[1]).toContain(
|
||||
fakeConfiguration
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('registerExtension()', () => {
|
||||
|
||||
@@ -20,9 +20,11 @@ import user from './user.js';
|
||||
import utils from './utils/';
|
||||
|
||||
import {
|
||||
createUINotificationService,
|
||||
createUIModalService,
|
||||
createUIDialogService,
|
||||
UINotificationService,
|
||||
UIModalService,
|
||||
UIDialogService,
|
||||
UIContextMenuService,
|
||||
UILabellingFlowService,
|
||||
} from './services';
|
||||
|
||||
const OHIF = {
|
||||
@@ -50,9 +52,11 @@ const OHIF = {
|
||||
measurements,
|
||||
hangingProtocols,
|
||||
//
|
||||
createUINotificationService,
|
||||
createUIModalService,
|
||||
createUIDialogService,
|
||||
UINotificationService,
|
||||
UIModalService,
|
||||
UIDialogService,
|
||||
UIContextMenuService,
|
||||
UILabellingFlowService,
|
||||
};
|
||||
|
||||
export {
|
||||
@@ -79,9 +83,11 @@ export {
|
||||
measurements,
|
||||
hangingProtocols,
|
||||
//
|
||||
createUINotificationService,
|
||||
createUIModalService,
|
||||
createUIDialogService,
|
||||
UINotificationService,
|
||||
UIModalService,
|
||||
UIDialogService,
|
||||
UIContextMenuService,
|
||||
UILabellingFlowService,
|
||||
};
|
||||
|
||||
export { OHIF };
|
||||
|
||||
@@ -10,9 +10,11 @@ describe('Top level exports', () => {
|
||||
'HotkeysManager',
|
||||
'ServicesManager',
|
||||
//
|
||||
'createUINotificationService',
|
||||
'createUIModalService',
|
||||
'createUIDialogService',
|
||||
'UINotificationService',
|
||||
'UIModalService',
|
||||
'UIDialogService',
|
||||
'UIContextMenuService',
|
||||
'UILabellingFlowService',
|
||||
//
|
||||
'utils',
|
||||
'studies',
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import cornerstoneTools from 'cornerstone-tools';
|
||||
import cornerstone from 'cornerstone-core';
|
||||
import log from '../../log';
|
||||
import getLabel from '../lib/getLabel';
|
||||
import getDescription from '../lib/getDescription';
|
||||
@@ -7,13 +8,32 @@ import guid from '../../utils/guid';
|
||||
import studyMetadataManager from '../../utils/studyMetadataManager';
|
||||
import { measurementApiDefaultConfig } from './../configuration.js';
|
||||
|
||||
|
||||
const configuration = {
|
||||
...measurementApiDefaultConfig,
|
||||
...measurementApiDefaultConfig
|
||||
};
|
||||
|
||||
export default class MeasurementApi {
|
||||
static Instance;
|
||||
|
||||
/**
|
||||
* Set configuration: It should merge default configuration with any new one
|
||||
*
|
||||
* @static
|
||||
* @param {Object} config
|
||||
* @param {Object} config.server
|
||||
* @param {string} config.server.type - The server type
|
||||
* @param {string} config.server.wadoRoot - The server wado URL root
|
||||
* @param {Array} config.measurementTools
|
||||
* @param {string} config.measurementTools[].id - The tool group id
|
||||
* @param {string} config.measurementTools[].name - The tool group name
|
||||
* @param {Array} config.measurementTools[].childTools - The child tool's configuration
|
||||
* @param {Object} config.dataExchange
|
||||
* @param {Function} config.dataExchange.store - Function that store measurement data
|
||||
* @param {Function} config.dataExchange.retrieve - Function that retrieves measurement data
|
||||
*
|
||||
* @memberof MeasurementApi
|
||||
*/
|
||||
static setConfiguration(config) {
|
||||
Object.assign(configuration, config);
|
||||
}
|
||||
@@ -203,13 +223,14 @@ export default class MeasurementApi {
|
||||
|
||||
retrieveMeasurements(patientId, timepointIds) {
|
||||
const retrievalFn = configuration.dataExchange.retrieve;
|
||||
const { server } = configuration;
|
||||
if (typeof retrievalFn !== 'function') {
|
||||
log.error('Measurement retrieval function has not been configured.');
|
||||
return;
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
retrievalFn(patientId, timepointIds).then(measurementData => {
|
||||
retrievalFn(server).then(measurementData => {
|
||||
if (measurementData) {
|
||||
log.info('Measurement data retrieval');
|
||||
log.info(measurementData);
|
||||
@@ -230,6 +251,10 @@ export default class MeasurementApi {
|
||||
// Synchronize the new tool data
|
||||
this.syncMeasurementsAndToolData();
|
||||
|
||||
cornerstone.getEnabledElements().forEach(enabledElement => {
|
||||
cornerstone.updateImage(enabledElement.element);
|
||||
});
|
||||
|
||||
// Let others know that the measurements are updated
|
||||
this.onMeasurementsUpdated();
|
||||
}, reject);
|
||||
@@ -237,6 +262,7 @@ export default class MeasurementApi {
|
||||
}
|
||||
|
||||
storeMeasurements(timepointId) {
|
||||
const { server } = configuration;
|
||||
const storeFn = configuration.dataExchange.store;
|
||||
if (typeof storeFn !== 'function') {
|
||||
log.error('Measurement store function has not been configured.');
|
||||
@@ -278,8 +304,9 @@ export default class MeasurementApi {
|
||||
};
|
||||
|
||||
log.info('Saving Measurements for timepoints:', timepoints);
|
||||
return storeFn(measurementData, filter).then(() => {
|
||||
return storeFn(measurementData, filter, server).then(result => {
|
||||
log.info('Measurement storage completed');
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -7,10 +7,12 @@ export default class ServicesManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a new service.
|
||||
*
|
||||
* @param {Object} service
|
||||
* @param {Object} configuration
|
||||
*/
|
||||
registerService(service) {
|
||||
registerService(service, configuration = {}) {
|
||||
if (!service) {
|
||||
log.warn(
|
||||
'Attempting to register a null/undefined service. Exiting early.'
|
||||
@@ -18,32 +20,45 @@ export default class ServicesManager {
|
||||
return;
|
||||
}
|
||||
|
||||
let serviceName = service.name;
|
||||
|
||||
if (!serviceName) {
|
||||
if (!service.name) {
|
||||
log.warn(`Service name not set. Exiting early.`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.registeredServiceNames.includes(serviceName)) {
|
||||
if (this.registeredServiceNames.includes(service.name)) {
|
||||
log.warn(
|
||||
`Extension name ${serviceName} has already been registered. Exiting before duplicating services.`
|
||||
`Service name ${service.name} has already been registered. Exiting before duplicating services.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
this.services[service.name] = service;
|
||||
if (service.create) {
|
||||
this.services[service.name] = service.create({ configuration });
|
||||
} else {
|
||||
log.warn(`Service create factory function not defined. Exiting early.`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Track service registration
|
||||
this.registeredServiceNames.push(serviceName);
|
||||
/* Track service registration */
|
||||
this.registeredServiceNames.push(service.name);
|
||||
}
|
||||
|
||||
/**
|
||||
* An array of services.
|
||||
* An array of services, or an array of arrays that contains service
|
||||
* configuration pairs.
|
||||
*
|
||||
* @param {Object[]} services - Array of services
|
||||
*/
|
||||
registerServices(services) {
|
||||
services.forEach(service => this.registerService(service));
|
||||
services.forEach(service => {
|
||||
const hasConfiguration = Array.isArray(service);
|
||||
|
||||
if (hasConfiguration) {
|
||||
const [ohifService, configuration] = service;
|
||||
this.registerService(ohifService, configuration);
|
||||
} else {
|
||||
this.registerService(service);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -16,18 +16,32 @@ describe('ServicesManager.js', () => {
|
||||
it('calls registerService() for each service', () => {
|
||||
servicesManager.registerService = jest.fn();
|
||||
|
||||
const fakeServices = [
|
||||
{ name: 'UINotificationTestService', hide: jest.fn() },
|
||||
{ name: 'UIModalTestService', hide: jest.fn() },
|
||||
];
|
||||
|
||||
servicesManager.registerServices(fakeServices);
|
||||
servicesManager.registerServices([
|
||||
{ name: 'UINotificationTestService', create: jest.fn() },
|
||||
{ name: 'UIModalTestService', create: jest.fn() },
|
||||
]);
|
||||
|
||||
expect(servicesManager.registerService.mock.calls.length).toBe(2);
|
||||
});
|
||||
|
||||
it('calls registerService() for each service passing its configuration if tuple', () => {
|
||||
servicesManager.registerService = jest.fn();
|
||||
const fakeConfiguration = { testing: true };
|
||||
|
||||
servicesManager.registerServices([
|
||||
{ name: 'UINotificationTestService', create: jest.fn() },
|
||||
[{ name: 'UIModalTestService', create: jest.fn() }, fakeConfiguration],
|
||||
]);
|
||||
|
||||
expect(servicesManager.registerService.mock.calls[1]).toContain(
|
||||
fakeConfiguration
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('registerService()', () => {
|
||||
const fakeService = { name: 'UINotificationService', create: jest.fn() };
|
||||
|
||||
it('logs a warning if the service is null or undefined', () => {
|
||||
const undefinedService = undefined;
|
||||
const nullService = null;
|
||||
@@ -39,8 +53,8 @@ describe('ServicesManager.js', () => {
|
||||
});
|
||||
|
||||
it('logs a warning if the service does not have a name', () => {
|
||||
const serviceWithEmptyName = { name: '', hide: jest.fn() };
|
||||
const serviceWithoutName = { hide: jest.fn() };
|
||||
const serviceWithEmptyName = { name: '', create: jest.fn() };
|
||||
const serviceWithoutName = { create: jest.fn() };
|
||||
|
||||
servicesManager.registerService(serviceWithEmptyName);
|
||||
servicesManager.registerService(serviceWithoutName);
|
||||
@@ -48,23 +62,37 @@ describe('ServicesManager.js', () => {
|
||||
expect(log.warn.mock.calls.length).toBe(2);
|
||||
});
|
||||
|
||||
it('tracks which services have been registered', () => {
|
||||
const service = {
|
||||
name: 'UINotificationService',
|
||||
};
|
||||
it('logs a warning if the service does not have a create factory function', () => {
|
||||
const serviceWithoutCreate = { name: 'UINotificationService' };
|
||||
|
||||
servicesManager.registerService(service);
|
||||
|
||||
expect(servicesManager.registeredServiceNames).toContain(service.name);
|
||||
});
|
||||
|
||||
it('logs a warning if the service has an name that has already been registered', () => {
|
||||
const service = { name: 'UINotificationService' };
|
||||
|
||||
servicesManager.registerService(service);
|
||||
servicesManager.registerService(service);
|
||||
servicesManager.registerService(serviceWithoutCreate);
|
||||
|
||||
expect(log.warn.mock.calls.length).toBe(1);
|
||||
});
|
||||
|
||||
it('tracks which services have been registered', () => {
|
||||
servicesManager.registerService(fakeService);
|
||||
|
||||
expect(servicesManager.registeredServiceNames).toContain(
|
||||
fakeService.name
|
||||
);
|
||||
});
|
||||
|
||||
it('logs a warning if the service has an name that has already been registered', () => {
|
||||
servicesManager.registerService(fakeService);
|
||||
servicesManager.registerService(fakeService);
|
||||
|
||||
expect(log.warn.mock.calls.length).toBe(1);
|
||||
});
|
||||
|
||||
it('pass configuration to service create factory function', () => {
|
||||
const configuration = { config: 'Some configuration' };
|
||||
|
||||
servicesManager.registerService(fakeService, configuration);
|
||||
|
||||
expect(fakeService.create.mock.calls[0][0]).toEqual({
|
||||
configuration,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* UI Context Menu
|
||||
*
|
||||
* @typedef {Object} ContextMenuProps
|
||||
* @property {Event} event The event with tool information.
|
||||
*/
|
||||
|
||||
const name = 'UIContextMenuService';
|
||||
|
||||
const publicAPI = {
|
||||
name,
|
||||
hide: _hide,
|
||||
show: _show,
|
||||
setServiceImplementation,
|
||||
};
|
||||
|
||||
const serviceImplementation = {
|
||||
_show: () => console.warn('show() NOT IMPLEMENTED'),
|
||||
_hide: () => console.warn('hide() NOT IMPLEMENTED'),
|
||||
};
|
||||
|
||||
/**
|
||||
* Show a new UI ContextMenu dialog;
|
||||
*
|
||||
* @param {ContextMenuProps} props { event }
|
||||
*/
|
||||
function _show({ event }) {
|
||||
return serviceImplementation._show({
|
||||
event,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide a UI ContextMenu dialog;
|
||||
*
|
||||
*/
|
||||
function _hide() {
|
||||
return serviceImplementation._hide();
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @param {*} {
|
||||
* show: showImplementation,
|
||||
* hide: hideImplementation,
|
||||
* }
|
||||
*/
|
||||
function setServiceImplementation({
|
||||
show: showImplementation,
|
||||
hide: hideImplementation,
|
||||
}) {
|
||||
if (showImplementation) {
|
||||
serviceImplementation._show = showImplementation;
|
||||
}
|
||||
if (hideImplementation) {
|
||||
serviceImplementation._hide = hideImplementation;
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
name,
|
||||
create: ({ configuration = {} }) => {
|
||||
return publicAPI;
|
||||
},
|
||||
};
|
||||
@@ -17,59 +17,60 @@
|
||||
* @property {Object} contentProps The dialog content props.
|
||||
* @property {boolean} [isDraggable=true] Controls if dialog content is draggable or not.
|
||||
* @property {boolean} [showOverlay=false] Controls dialog overlay.
|
||||
* @property {boolean} [centralize=false] Center the dialog on the screen.
|
||||
* @property {boolean} [preservePosition=true] Use last position instead of default.
|
||||
* @property {ElementPosition} defaultPosition Specifies the `x` and `y` that the dragged item should start at.
|
||||
* @property {ElementPosition} position If this property is present, the item becomes 'controlled' and is not responsive to user input.
|
||||
* @property {Function} onStart Called when dragging starts. If `false` is returned any handler, the action will cancel.
|
||||
* @property {Function} onStop Called when dragging stops.
|
||||
* @property {Function} onDrag Called while dragging.
|
||||
*/
|
||||
|
||||
const uiDialogServicePublicAPI = {
|
||||
name: 'UIDialogService',
|
||||
dismiss,
|
||||
dismissAll,
|
||||
create,
|
||||
const name = 'UIDialogService';
|
||||
|
||||
const publicAPI = {
|
||||
name,
|
||||
dismiss: _dismiss,
|
||||
dismissAll: _dismissAll,
|
||||
create: _create,
|
||||
setServiceImplementation,
|
||||
};
|
||||
|
||||
const uiDialogServiceImplementation = {
|
||||
const serviceImplementation = {
|
||||
_dismiss: () => console.warn('dismiss() NOT IMPLEMENTED'),
|
||||
_dismissAll: () => console.warn('dismissAll() NOT IMPLEMENTED'),
|
||||
_create: () => console.warn('create() NOT IMPLEMENTED'),
|
||||
};
|
||||
|
||||
function createUIDialogService() {
|
||||
return uiDialogServicePublicAPI;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a new UI dialog;
|
||||
*
|
||||
* @param {DialogProps} props { id, content, contentProps, onStart, onDrag, onStop, isDraggable, showOverlay, defaultPosition, position }
|
||||
* @param {DialogProps} props { id, content, contentProps, onStart, onDrag, onStop, centralize, isDraggable, showOverlay, preservePosition, defaultPosition }
|
||||
*/
|
||||
function create({
|
||||
function _create({
|
||||
id,
|
||||
content,
|
||||
contentProps,
|
||||
onStart,
|
||||
onDrag,
|
||||
onStop,
|
||||
centralize = false,
|
||||
preservePosition = true,
|
||||
isDraggable = true,
|
||||
showOverlay = false,
|
||||
defaultPosition,
|
||||
position,
|
||||
}) {
|
||||
return uiDialogServiceImplementation._create({
|
||||
return serviceImplementation._create({
|
||||
id,
|
||||
content,
|
||||
contentProps,
|
||||
onStart,
|
||||
onDrag,
|
||||
onStop,
|
||||
centralize,
|
||||
preservePosition,
|
||||
isDraggable,
|
||||
showOverlay,
|
||||
defaultPosition,
|
||||
position,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -78,8 +79,8 @@ function create({
|
||||
*
|
||||
* @returns void
|
||||
*/
|
||||
function dismissAll() {
|
||||
return uiDialogServiceImplementation._dismissAll();
|
||||
function _dismissAll() {
|
||||
return serviceImplementation._dismissAll();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -87,8 +88,8 @@ function dismissAll() {
|
||||
*
|
||||
* @returns void
|
||||
*/
|
||||
function dismiss({ id }) {
|
||||
return uiDialogServiceImplementation._dismiss({ id });
|
||||
function _dismiss({ id }) {
|
||||
return serviceImplementation._dismiss({ id });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -106,14 +107,19 @@ function setServiceImplementation({
|
||||
create: createImplementation,
|
||||
}) {
|
||||
if (dismissImplementation) {
|
||||
uiDialogServiceImplementation._dismiss = dismissImplementation;
|
||||
serviceImplementation._dismiss = dismissImplementation;
|
||||
}
|
||||
if (dismissAllImplementation) {
|
||||
uiDialogServiceImplementation._dismissAll = dismissAllImplementation;
|
||||
serviceImplementation._dismissAll = dismissAllImplementation;
|
||||
}
|
||||
if (createImplementation) {
|
||||
uiDialogServiceImplementation._create = createImplementation;
|
||||
serviceImplementation._create = createImplementation;
|
||||
}
|
||||
}
|
||||
|
||||
export default createUIDialogService;
|
||||
export default {
|
||||
name,
|
||||
create: ({ configuration = {} }) => {
|
||||
return publicAPI;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* UI Labelling Flow
|
||||
*
|
||||
* @typedef {Object} LabellingFlowProps
|
||||
* @property {Object} defaultPosition The position of the labelling dialog.
|
||||
* @property {boolean} centralize conditional to center the labelling dialog.
|
||||
* @property {Object} props The labelling props.
|
||||
*
|
||||
*/
|
||||
|
||||
const name = 'UILabellingFlowService';
|
||||
|
||||
const publicAPI = {
|
||||
name,
|
||||
show: _show,
|
||||
hide: _hide,
|
||||
setServiceImplementation,
|
||||
};
|
||||
|
||||
const serviceImplementation = {
|
||||
_show: () => console.warn('show() NOT IMPLEMENTED'),
|
||||
_hide: () => console.warn('hide() NOT IMPLEMENTED'),
|
||||
};
|
||||
|
||||
/**
|
||||
* Hide a UI LabellingFlow dialog;
|
||||
*
|
||||
*/
|
||||
function _hide() {
|
||||
return serviceImplementation._hide();
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a new UI LabellingFlow dialog;
|
||||
*
|
||||
* @param {LabellingFlowProps} props { defaultPosition, centralize, props }
|
||||
*/
|
||||
function _show({ defaultPosition, centralize, props }) {
|
||||
return serviceImplementation._show({
|
||||
defaultPosition,
|
||||
centralize,
|
||||
props,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @param {*} {
|
||||
* show: showImplementation,
|
||||
* hide: hideImplementation,
|
||||
* }
|
||||
*/
|
||||
function setServiceImplementation({
|
||||
show: showImplementation,
|
||||
hide: hideImplementation,
|
||||
}) {
|
||||
if (showImplementation) {
|
||||
serviceImplementation._show = showImplementation;
|
||||
}
|
||||
if (hideImplementation) {
|
||||
serviceImplementation._hide = hideImplementation;
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
name,
|
||||
create: ({ configuration = {} }) => {
|
||||
return publicAPI;
|
||||
},
|
||||
};
|
||||
@@ -11,28 +11,26 @@
|
||||
* @property {string} [customClassName=null] The custom class to style the modal.
|
||||
*/
|
||||
|
||||
const uiModalServicePublicAPI = {
|
||||
name: 'UIModalService',
|
||||
hide,
|
||||
show,
|
||||
const name = 'UIModalService';
|
||||
|
||||
const publicAPI = {
|
||||
name,
|
||||
hide: _hide,
|
||||
show: _show,
|
||||
setServiceImplementation,
|
||||
};
|
||||
|
||||
const uiModalServiceImplementation = {
|
||||
const serviceImplementation = {
|
||||
_hide: () => console.warn('hide() NOT IMPLEMENTED'),
|
||||
_show: () => console.warn('show() NOT IMPLEMENTED'),
|
||||
};
|
||||
|
||||
function createUIModalService() {
|
||||
return uiModalServicePublicAPI;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a new UI modal;
|
||||
*
|
||||
* @param {ModalProps} props { content, contentProps, shouldCloseOnEsc, isOpen, closeButton, title, customClassName }
|
||||
*/
|
||||
function show({
|
||||
function _show({
|
||||
content = null,
|
||||
contentProps = null,
|
||||
shouldCloseOnEsc = false,
|
||||
@@ -41,7 +39,7 @@ function show({
|
||||
title = null,
|
||||
customClassName = null,
|
||||
}) {
|
||||
return uiModalServiceImplementation._show({
|
||||
return serviceImplementation._show({
|
||||
content,
|
||||
contentProps,
|
||||
shouldCloseOnEsc,
|
||||
@@ -57,8 +55,8 @@ function show({
|
||||
*
|
||||
* @returns void
|
||||
*/
|
||||
function hide() {
|
||||
return uiModalServiceImplementation._hide();
|
||||
function _hide() {
|
||||
return serviceImplementation._hide();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -74,11 +72,16 @@ function setServiceImplementation({
|
||||
show: showImplementation,
|
||||
}) {
|
||||
if (hideImplementation) {
|
||||
uiModalServiceImplementation._hide = hideImplementation;
|
||||
serviceImplementation._hide = hideImplementation;
|
||||
}
|
||||
if (showImplementation) {
|
||||
uiModalServiceImplementation._show = showImplementation;
|
||||
serviceImplementation._show = showImplementation;
|
||||
}
|
||||
}
|
||||
|
||||
export default createUIModalService;
|
||||
export default {
|
||||
name,
|
||||
create: ({ configuration = {} }) => {
|
||||
return publicAPI;
|
||||
},
|
||||
};
|
||||
@@ -10,22 +10,20 @@
|
||||
* @property {boolean} [autoClose=true]
|
||||
*/
|
||||
|
||||
const uiNotificationServicePublicAPI = {
|
||||
name: 'UINotificationService',
|
||||
hide,
|
||||
show,
|
||||
const name = 'UINotificationService';
|
||||
|
||||
const publicAPI = {
|
||||
name,
|
||||
hide: _hide,
|
||||
show: _show,
|
||||
setServiceImplementation,
|
||||
};
|
||||
|
||||
const uiNotificationServiceImplementation = {
|
||||
const serviceImplementation = {
|
||||
_hide: () => console.warn('hide() NOT IMPLEMENTED'),
|
||||
_show: () => console.warn('show() NOT IMPLEMENTED'),
|
||||
};
|
||||
|
||||
function createUINotificationService() {
|
||||
return uiNotificationServicePublicAPI;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and show a new UI notification; returns the
|
||||
* ID of the created notification.
|
||||
@@ -33,7 +31,7 @@ function createUINotificationService() {
|
||||
* @param {Notification} notification { title, message, duration, position, type, autoClose}
|
||||
* @returns {number} id
|
||||
*/
|
||||
function show({
|
||||
function _show({
|
||||
title,
|
||||
message,
|
||||
duration = 5000,
|
||||
@@ -41,7 +39,7 @@ function show({
|
||||
type = 'info',
|
||||
autoClose = true,
|
||||
}) {
|
||||
return uiNotificationServiceImplementation._show({
|
||||
return serviceImplementation._show({
|
||||
title,
|
||||
message,
|
||||
duration,
|
||||
@@ -57,8 +55,8 @@ function show({
|
||||
* @param {number} id - id of the notification to hide/dismiss
|
||||
* @returns undefined
|
||||
*/
|
||||
function hide(id) {
|
||||
return uiNotificationServiceImplementation._hide({ id });
|
||||
function _hide(id) {
|
||||
return serviceImplementation._hide({ id });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -74,11 +72,16 @@ function setServiceImplementation({
|
||||
show: showImplementation,
|
||||
}) {
|
||||
if (hideImplementation) {
|
||||
uiNotificationServiceImplementation._hide = hideImplementation;
|
||||
serviceImplementation._hide = hideImplementation;
|
||||
}
|
||||
if (showImplementation) {
|
||||
uiNotificationServiceImplementation._show = showImplementation;
|
||||
serviceImplementation._show = showImplementation;
|
||||
}
|
||||
}
|
||||
|
||||
export default createUINotificationService;
|
||||
export default {
|
||||
name,
|
||||
create: ({ configuration = {} }) => {
|
||||
return publicAPI;
|
||||
},
|
||||
};
|
||||
@@ -1,11 +1,15 @@
|
||||
import ServicesManager from './ServicesManager.js';
|
||||
import createUINotificationService from './UINotificationService';
|
||||
import createUIModalService from './UIModalService';
|
||||
import createUIDialogService from './UIDialogService';
|
||||
import UINotificationService from './UINotificationService';
|
||||
import UIModalService from './UIModalService';
|
||||
import UIDialogService from './UIDialogService';
|
||||
import UIContextMenuService from './UIContextMenuService';
|
||||
import UILabellingFlowService from './UILabellingFlowService';
|
||||
|
||||
export {
|
||||
createUINotificationService,
|
||||
createUIModalService,
|
||||
createUIDialogService,
|
||||
UINotificationService,
|
||||
UIModalService,
|
||||
UIDialogService,
|
||||
UIContextMenuService,
|
||||
UILabellingFlowService,
|
||||
ServicesManager,
|
||||
};
|
||||
@@ -3,6 +3,17 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [0.52.3](https://github.com/OHIF/Viewers/compare/@ohif/i18n@0.52.2...@ohif/i18n@0.52.3) (2019-12-12)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* translations ([#1234](https://github.com/OHIF/Viewers/issues/1234)) ([30b9e44](https://github.com/OHIF/Viewers/commit/30b9e4422073557287ef26a80b38eeb3f3fcff4c))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.52.2](https://github.com/OHIF/Viewers/compare/@ohif/i18n@0.52.1...@ohif/i18n@0.52.2) (2019-11-28)
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/i18n",
|
||||
"version": "0.52.2",
|
||||
"version": "0.52.3",
|
||||
"description": "Internationalization library for The OHIF Viewer",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
"Clear": "Clear",
|
||||
"Coronal": "Coronal",
|
||||
"Crosshairs": "Crosshairs",
|
||||
"Download": "Download",
|
||||
"Ellipse": "Ellipse",
|
||||
"Elliptical": "Elliptical",
|
||||
"Flip H": "Flip H",
|
||||
|
||||
@@ -10,18 +10,19 @@
|
||||
"Clear": "Limpiar",
|
||||
"Coronal": "Coronal",
|
||||
"Crosshairs": "Punto de mira",
|
||||
"Download": "Descargar",
|
||||
"Ellipse": "Elipse",
|
||||
"Elliptical": "Elíptico",
|
||||
"Flip H": "Espejo H",
|
||||
"Flip V": "Espejo V",
|
||||
"Flip H": "Voltear H",
|
||||
"Flip V": "Voltear V",
|
||||
"Freehand": "Mano alzada",
|
||||
"Invert": "Invertir",
|
||||
"Invert": "Negativo",
|
||||
"Layout": "$t(Common:Layout)",
|
||||
"Length": "Longitud",
|
||||
"Levels": "Niveles",
|
||||
"Magnify": "Aumentar",
|
||||
"Levels": "W/L",
|
||||
"Magnify": "Lupa",
|
||||
"Manual": "Manual",
|
||||
"Measurements": "Mediciones",
|
||||
"Measurements": "Medidas",
|
||||
"More": "$t(Common:More)",
|
||||
"Next": "$t(Common:Next)",
|
||||
"Pan": "Mover",
|
||||
@@ -32,11 +33,11 @@
|
||||
"Rectangle": "Rectángulo",
|
||||
"Reset": "$t(Common:Reset)",
|
||||
"Reset to Defaults": "$t(Common:Reset) por defecto",
|
||||
"Rotate Right": "Rotar ->",
|
||||
"Rotate Right": "Girar ->",
|
||||
"Sagittal": "Sagital",
|
||||
"Save": "Guardar",
|
||||
"Stack Scroll": "Scroll en pila",
|
||||
"Stack Scroll": "Scroll",
|
||||
"Stop": "$t(Common:Stop)",
|
||||
"Themes": "Temas",
|
||||
"Zoom": "Zoom"
|
||||
"Zoom": "Ampliar"
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"Image": "Imagen",
|
||||
"Layout": "Disposición",
|
||||
"Layout": "Formato",
|
||||
"Measurements": "Medidas",
|
||||
"More": "Más",
|
||||
"Next": "Siguiente",
|
||||
"Play": "Play",
|
||||
"Previous": "Anterior",
|
||||
"Reset": "Resetear",
|
||||
"Reset": "Restaurar",
|
||||
"RowsPerPage": "filas por página",
|
||||
"Series": "Secuencia",
|
||||
"Show": "Mostrar",
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
"Clear": "Limpar",
|
||||
"Coronal": "Coronal",
|
||||
"Crosshairs": "Localizador",
|
||||
"Download": "Baixar",
|
||||
"Ellipse": "Elipse",
|
||||
"Elliptical": "Elíptico",
|
||||
"Flip H": "Inverter H",
|
||||
@@ -30,8 +31,8 @@
|
||||
"Probe": "Prova",
|
||||
"ROI Window": "Janela ROI",
|
||||
"Rectangle": "Retângulo",
|
||||
"Reset": "Resetar",
|
||||
"Reset to Defaults": "Restaurar para o Padrão",
|
||||
"Reset": "$t(Common:Reset)",
|
||||
"Reset to Defaults": "$t(Common:Reset) para o Padrão",
|
||||
"Rotate Right": "Girar à direita",
|
||||
"Sagittal": "Sagital",
|
||||
"Save": "Salvar",
|
||||
|
||||
@@ -6,6 +6,6 @@
|
||||
"Next": "Próximo",
|
||||
"Play": "Play",
|
||||
"Previous": "Anterior",
|
||||
"Reset": "Reset",
|
||||
"Reset": "Restaurar",
|
||||
"Stop": "Stop"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"Cancel": "Cancelar",
|
||||
"Reset to Defaults": "Restaurar Default",
|
||||
"Reset to Defaults": "$t(Common:Reset) para Padrão",
|
||||
"ResetDefaultMessage": "Preferências resetadas com sucesso. <br /> Você deve <strong>Salvar</strong> para que essa ação seja realizada.",
|
||||
"Save": "Salvar",
|
||||
"SaveMessage": "Preferências salvas",
|
||||
|
||||
@@ -3,6 +3,58 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.1.2](https://github.com/OHIF/Viewers/compare/@ohif/ui@1.1.1...@ohif/ui@1.1.2) (2019-12-12)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* translations ([#1234](https://github.com/OHIF/Viewers/issues/1234)) ([30b9e44](https://github.com/OHIF/Viewers/commit/30b9e4422073557287ef26a80b38eeb3f3fcff4c))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.1.1](https://github.com/OHIF/Viewers/compare/@ohif/ui@1.1.0...@ohif/ui@1.1.1) (2019-12-11)
|
||||
|
||||
**Note:** Version bump only for package @ohif/ui
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [1.1.0](https://github.com/OHIF/Viewers/compare/@ohif/ui@1.0.1...@ohif/ui@1.1.0) (2019-12-11)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* 🎸 DICOM SR STOW on MeasurementAPI ([#954](https://github.com/OHIF/Viewers/issues/954)) ([ebe1af8](https://github.com/OHIF/Viewers/commit/ebe1af8d4f75d2483eba869655906d7829bd9666)), closes [#758](https://github.com/OHIF/Viewers/issues/758)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.0.1](https://github.com/OHIF/Viewers/compare/@ohif/ui@1.0.0...@ohif/ui@1.0.1) (2019-12-09)
|
||||
|
||||
**Note:** Version bump only for package @ohif/ui
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [1.0.0](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.65.4...@ohif/ui@1.0.0) (2019-12-09)
|
||||
|
||||
|
||||
* feat!: Ability to configure cornerstone tools via extension configuration (#1229) ([55a5806](https://github.com/OHIF/Viewers/commit/55a580659ecb74ca6433461d8f9a05c2a2b69533)), closes [#1229](https://github.com/OHIF/Viewers/issues/1229)
|
||||
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* modifies the exposed react <App /> components props. The contract for providing configuration for the app has changed. Please reference updated documentation for guidance.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.65.4](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.65.3...@ohif/ui@0.65.4) (2019-12-07)
|
||||
|
||||
**Note:** Version bump only for package @ohif/ui
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/ui",
|
||||
"version": "0.65.4",
|
||||
"version": "1.1.2",
|
||||
"description": "A set of React components for Medical Imaging Viewers",
|
||||
"author": "OHIF Contributors",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -32,7 +32,7 @@ const ViewportDownloadForm = ({
|
||||
maximumSize,
|
||||
canvasClass,
|
||||
}) => {
|
||||
const [t] = useTranslation('ViewportDownloadForm');
|
||||
const [t] = useTranslation('Buttons');
|
||||
|
||||
const [filename, setFilename] = useState(DEFAULT_FILENAME);
|
||||
const [fileType, setFileType] = useState('jpg');
|
||||
@@ -196,11 +196,12 @@ const ViewportDownloadForm = ({
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="file-info-container">
|
||||
<div className="file-info-container" data-cy="file-info-container">
|
||||
<div className="col">
|
||||
<div className="width">
|
||||
<TextInput
|
||||
type="number"
|
||||
data-cy="image-width"
|
||||
min={minimumSize}
|
||||
max={maximumSize}
|
||||
value={width}
|
||||
@@ -211,6 +212,7 @@ const ViewportDownloadForm = ({
|
||||
<div className="height">
|
||||
<TextInput
|
||||
type="number"
|
||||
data-cy="image-height"
|
||||
min={minimumSize}
|
||||
max={maximumSize}
|
||||
value={height}
|
||||
@@ -224,6 +226,7 @@ const ViewportDownloadForm = ({
|
||||
<div className="file-name">
|
||||
<TextInput
|
||||
type="text"
|
||||
data-cy="file-name"
|
||||
value={filename}
|
||||
onChange={event => setFilename(event.target.value)}
|
||||
label={t('File name')}
|
||||
@@ -233,6 +236,7 @@ const ViewportDownloadForm = ({
|
||||
<div className="file-type">
|
||||
<Select
|
||||
value={fileType}
|
||||
data-cy="file-type"
|
||||
onChange={event => setFileType(event.target.value)}
|
||||
options={FILE_TYPE_OPTIONS}
|
||||
label={t('File type')}
|
||||
@@ -245,6 +249,7 @@ const ViewportDownloadForm = ({
|
||||
<label htmlFor="show-annotations" className="form-check-label">
|
||||
<input
|
||||
id="show-annotations"
|
||||
data-cy="show-annotations"
|
||||
type="checkbox"
|
||||
className="form-check-input"
|
||||
checked={showAnnotations}
|
||||
@@ -278,23 +283,33 @@ const ViewportDownloadForm = ({
|
||||
></canvas>
|
||||
</div>
|
||||
|
||||
<div className="preview">
|
||||
<div className="preview" data-cy="image-preview">
|
||||
<h4> {t('Image Preview')}</h4>
|
||||
<img
|
||||
className="viewport-preview"
|
||||
src={viewportPreview.src}
|
||||
alt="Viewport Preview"
|
||||
data-cy="viewport-preview-img"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="actions">
|
||||
<div className="action-cancel">
|
||||
<button type="button" className="btn btn-danger" onClick={onClose}>
|
||||
<button
|
||||
type="button"
|
||||
data-cy="cancel-btn"
|
||||
className="btn btn-danger"
|
||||
onClick={onClose}
|
||||
>
|
||||
{t('Cancel')}
|
||||
</button>
|
||||
</div>
|
||||
<div className="action-save">
|
||||
<button onClick={downloadImage} className="btn btn-primary">
|
||||
<button
|
||||
onClick={downloadImage}
|
||||
className="btn btn-primary"
|
||||
data-cy="download-btn"
|
||||
>
|
||||
{t('Download')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -22,8 +22,9 @@ class MeasurementTable extends Component {
|
||||
onDeleteClick: PropTypes.func,
|
||||
onEditDescriptionClick: PropTypes.func,
|
||||
selectedMeasurementNumber: PropTypes.number,
|
||||
overwallWarnings: PropTypes.object,
|
||||
t: PropTypes.func,
|
||||
saveFunction: PropTypes.func,
|
||||
onSaveComplete: PropTypes.func,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
@@ -38,8 +39,9 @@ class MeasurementTable extends Component {
|
||||
};
|
||||
|
||||
render() {
|
||||
const hasOverallWarnings =
|
||||
this.props.overallWarnings.warningList.length > 0;
|
||||
const { overallWarnings, saveFunction, t } = this.props;
|
||||
const hasOverallWarnings = overallWarnings.warningList.length > 0;
|
||||
|
||||
return (
|
||||
<div className="measurementTable">
|
||||
<div className="measurementTableHeader">
|
||||
@@ -55,7 +57,7 @@ class MeasurementTable extends Component {
|
||||
style={{}}
|
||||
>
|
||||
<div className="warningTitle">
|
||||
{this.props.t('Criteria nonconformities')}
|
||||
{t('Criteria nonconformities')}
|
||||
</div>
|
||||
<div className="warningContent">
|
||||
{this.getWarningContent()}
|
||||
@@ -75,10 +77,42 @@ class MeasurementTable extends Component {
|
||||
<ScrollableArea>
|
||||
<div>{this.getMeasurementsGroups()}</div>
|
||||
</ScrollableArea>
|
||||
<div className="measurementTableFooter">
|
||||
{saveFunction && (
|
||||
<button onClick={this.saveFunction} className="saveBtn">
|
||||
<Icon name="save" width="14px" height="14px" />
|
||||
Save measurements
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
saveFunction = async event => {
|
||||
const { saveFunction, onSaveComplete } = this.props;
|
||||
if (saveFunction) {
|
||||
try {
|
||||
const result = await saveFunction();
|
||||
if (onSaveComplete) {
|
||||
onSaveComplete({
|
||||
title: 'STOW SR',
|
||||
message: result.message,
|
||||
type: 'success',
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
if (onSaveComplete) {
|
||||
onSaveComplete({
|
||||
title: 'STOW SR',
|
||||
message: error.message,
|
||||
type: 'error',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
getMeasurementsGroups = () => {
|
||||
return this.props.measurementCollection.map((measureGroup, index) => {
|
||||
return (
|
||||
@@ -157,7 +191,7 @@ class MeasurementTable extends Component {
|
||||
};
|
||||
|
||||
getWarningContent = () => {
|
||||
const { warningList = '' } = this.props.overwallWarnings;
|
||||
const { warningList = '' } = this.props.overallWarnings;
|
||||
|
||||
if (Array.isArray(warningList)) {
|
||||
const listedWarnings = warningList.map((warn, index) => {
|
||||
|
||||
@@ -1,92 +1,117 @@
|
||||
.measurementTable
|
||||
background-color: var(--primary-background-color)
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%
|
||||
width: 100%
|
||||
display: flex
|
||||
flex-direction: column
|
||||
|
||||
.maxMeasurements
|
||||
border-radius: 3px;
|
||||
color: var(--primary-background-color);
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
height: 19px;
|
||||
line-height: 17px;
|
||||
margin-top: 22px;
|
||||
margin-right: 15px;
|
||||
padding: 2px 6px 0;
|
||||
text-transform: uppercase;
|
||||
transition: all 0.3s ease;
|
||||
background-color: var(--default-color);
|
||||
border-radius: 3px
|
||||
color: var(--primary-background-color)
|
||||
font-size: 12px
|
||||
font-weight: 500
|
||||
height: 19px
|
||||
line-height: 17px
|
||||
margin-top: 22px
|
||||
margin-right: 15px
|
||||
padding: 2px 6px 0
|
||||
text-transform: uppercase
|
||||
transition: all 0.3s ease
|
||||
background-color: var(--default-color)
|
||||
|
||||
.measurementTableHeader
|
||||
padding-left: 45px;
|
||||
padding-left: 45px
|
||||
|
||||
.measurementTableHeaderItem
|
||||
display: inline-block;
|
||||
margin-top: 9px;
|
||||
margin-left: 9px;
|
||||
padding-left: 9px;
|
||||
width: 90px;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
border-left: 1px solid var(--text-secondary-color);
|
||||
color: var(--text-primary-color);
|
||||
display: inline-block
|
||||
margin-top: 9px
|
||||
margin-left: 9px
|
||||
padding-left: 9px
|
||||
width: 90px
|
||||
font-size: 14px
|
||||
line-height: 20px
|
||||
border-left: 1px solid var(--text-secondary-color)
|
||||
color: var(--text-primary-color)
|
||||
|
||||
.timepointLabel
|
||||
color: var(--text-secondary-color);
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary-color)
|
||||
font-size: 12px
|
||||
|
||||
.warning-status
|
||||
display: block;
|
||||
position: absolute;
|
||||
width: 50px;
|
||||
height: 40px;
|
||||
padding-left: 7px;
|
||||
left: 15px;
|
||||
top: 7px;
|
||||
display: block
|
||||
position: absolute
|
||||
width: 50px
|
||||
height: 40px
|
||||
padding-left: 7px
|
||||
left: 15px
|
||||
top: 7px
|
||||
|
||||
.warning-border
|
||||
display: block;
|
||||
border: 2px solid #e29e4a;
|
||||
border-radius: 36px;
|
||||
padding: 6px 7px;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: block
|
||||
border: 2px solid #e29e4a
|
||||
border-radius: 36px
|
||||
padding: 6px 7px
|
||||
width: 40px
|
||||
height: 40px
|
||||
|
||||
svg
|
||||
color: #e29e4a;
|
||||
width: 22px;
|
||||
height: 20px;
|
||||
pointer-events: inherit;
|
||||
color: #e29e4a
|
||||
width: 22px
|
||||
height: 20px
|
||||
pointer-events: inherit
|
||||
|
||||
.measurementTableFooter
|
||||
padding: 10px
|
||||
color: #fff
|
||||
text-align: center
|
||||
z-index: 1
|
||||
|
||||
.saveBtn
|
||||
border: 1px solid var(--ui-border-color-active)
|
||||
border-radius: 25px
|
||||
background-color: var(--active-color)
|
||||
color: var(--ui-gray-dark)
|
||||
line-height: 25px
|
||||
padding: 0 20px
|
||||
outline: none
|
||||
cursor: pointer
|
||||
|
||||
&:hover, &:active
|
||||
background-color: var(--ui-sky-blue)
|
||||
|
||||
svg
|
||||
margin-right: 4px
|
||||
position: relative
|
||||
top: 2px
|
||||
|
||||
|
||||
.tooltip-warning
|
||||
&.in
|
||||
opacity 1;
|
||||
opacity 1
|
||||
|
||||
.tooltip-inner
|
||||
padding: 0
|
||||
|
||||
.warningTitle
|
||||
text-align: left
|
||||
background-color: var(--ui-gray-darkest);
|
||||
padding: 7px 15px;
|
||||
border-top-left-radius: 7px;
|
||||
border-top-right-radius: 7px;
|
||||
background-color: var(--ui-gray-darkest)
|
||||
padding: 7px 15px
|
||||
border-top-left-radius: 7px
|
||||
border-top-right-radius: 7px
|
||||
|
||||
.warningContent
|
||||
text-align center
|
||||
background-color: var(--ui-gray);
|
||||
border-bottom-left-radius: 7px;
|
||||
border-bottom-right-radius: 7px;
|
||||
background-color: var(--ui-gray)
|
||||
border-bottom-left-radius: 7px
|
||||
border-bottom-right-radius: 7px
|
||||
|
||||
ol
|
||||
margin-top: auto;
|
||||
padding-inline-start: 20px;
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
padding-left: 25px;
|
||||
text-align: left;
|
||||
margin-top: auto
|
||||
padding-inline-start: 20px
|
||||
padding-top: 10px
|
||||
padding-bottom: 10px
|
||||
padding-left: 25px
|
||||
text-align: left
|
||||
|
||||
li
|
||||
padding-right: 10px;
|
||||
padding-right: 10px
|
||||
@@ -29,7 +29,7 @@ const OHIFModal = ({
|
||||
const renderHeader = () => {
|
||||
return (
|
||||
title && (
|
||||
<div className="OHIFModal__header">
|
||||
<div className="OHIFModal__header" data-cy="modal-header">
|
||||
<h4>{title}</h4>
|
||||
{closeButton && (
|
||||
<button data-cy="close-button" onClick={onClose}>
|
||||
@@ -44,6 +44,7 @@ const OHIFModal = ({
|
||||
return (
|
||||
<Modal
|
||||
className={classNames('OHIFModal', className)}
|
||||
data-cy="modal"
|
||||
shouldCloseOnEsc={shouldCloseOnEsc}
|
||||
isOpen={isOpen}
|
||||
title={title}
|
||||
@@ -51,7 +52,9 @@ const OHIFModal = ({
|
||||
>
|
||||
<>
|
||||
{renderHeader()}
|
||||
<div className="OHIFModal__content">{children}</div>
|
||||
<div className="OHIFModal__content" data-cy="modal-content">
|
||||
{children}
|
||||
</div>
|
||||
</>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
@@ -6,9 +6,7 @@
|
||||
position: relative
|
||||
|
||||
.simpleDialog
|
||||
position: fixed;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
position: relative;
|
||||
z-index: 1000;
|
||||
border: 0;
|
||||
border-radius: 6px;
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
import React, {
|
||||
createContext,
|
||||
useContext,
|
||||
useEffect,
|
||||
useCallback,
|
||||
} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { useDialog } from '@ohif/ui';
|
||||
import { useLabellingFlow } from '@ohif/ui';
|
||||
|
||||
const ContextMenuContext = createContext(null);
|
||||
const { Provider } = ContextMenuContext;
|
||||
|
||||
export const useContextMenu = () => useContext(ContextMenuContext);
|
||||
|
||||
const ContextMenuProvider = ({
|
||||
children,
|
||||
service,
|
||||
contextMenuComponent: ContextMenuComponent,
|
||||
onDelete,
|
||||
}) => {
|
||||
const { create, dismiss } = useDialog();
|
||||
const { show: showLabellingFlow } = useLabellingFlow();
|
||||
|
||||
/**
|
||||
* Sets the implementation of a context menu service that can be used by extensions.
|
||||
*
|
||||
* @returns void
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (service) {
|
||||
service.setServiceImplementation({
|
||||
show,
|
||||
hide,
|
||||
});
|
||||
}
|
||||
}, [hide, service, show]);
|
||||
|
||||
const hide = useCallback(() => dismiss({ id: 'context-menu' }), [dismiss]);
|
||||
|
||||
/**
|
||||
* Show the context menu and override its configuration props.
|
||||
*
|
||||
* @param {ContextMenuProps} props { eventData, isTouchEvent, onClose, visible }
|
||||
* @returns void
|
||||
*/
|
||||
const show = useCallback(
|
||||
({ event }) => {
|
||||
hide();
|
||||
create({
|
||||
id: 'context-menu',
|
||||
isDraggable: false,
|
||||
preservePosition: false,
|
||||
content: ContextMenuComponent,
|
||||
contentProps: {
|
||||
eventData: event,
|
||||
onDelete: (nearbyToolData, eventData) =>
|
||||
onDelete(nearbyToolData, eventData),
|
||||
onClose: () => dismiss({ id: 'context-menu' }),
|
||||
onSetLabel: (eventData, measurementData) =>
|
||||
showLabellingFlow({
|
||||
event: eventData,
|
||||
centralize: true,
|
||||
props: {
|
||||
measurementData,
|
||||
skipAddLabelButton: true,
|
||||
editLocation: true,
|
||||
},
|
||||
}),
|
||||
onSetDescription: (eventData, measurementData) =>
|
||||
showLabellingFlow({
|
||||
event: eventData,
|
||||
centralize: false,
|
||||
defaultPosition: _getDefaultPosition(eventData),
|
||||
props: {
|
||||
measurementData,
|
||||
editDescriptionOnDialog: true,
|
||||
},
|
||||
}),
|
||||
},
|
||||
defaultPosition: _getDefaultPosition(event),
|
||||
});
|
||||
},
|
||||
[ContextMenuComponent, create, dismiss, hide, onDelete, showLabellingFlow]
|
||||
);
|
||||
|
||||
const _getDefaultPosition = event => ({
|
||||
x: (event && event.currentPoints.client.x) || 0,
|
||||
y: (event && event.currentPoints.client.y) || 0,
|
||||
});
|
||||
|
||||
return (
|
||||
<Provider
|
||||
value={{
|
||||
show,
|
||||
hide,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</Provider>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Higher Order Component to use the context menu methods through a Class Component.
|
||||
*
|
||||
* @returns
|
||||
*/
|
||||
export const withContextMenu = Component => {
|
||||
return function WrappedComponent(props) {
|
||||
const { show, hide } = useContextMenu();
|
||||
return (
|
||||
<Component
|
||||
{...props}
|
||||
modal={{
|
||||
show,
|
||||
hide,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
ContextMenuProvider.defaultProps = {
|
||||
service: null,
|
||||
};
|
||||
|
||||
ContextMenuProvider.propTypes = {
|
||||
children: PropTypes.oneOfType([
|
||||
PropTypes.arrayOf(PropTypes.node),
|
||||
PropTypes.node,
|
||||
]).isRequired,
|
||||
service: PropTypes.shape({
|
||||
setServiceImplementation: PropTypes.func,
|
||||
}),
|
||||
contextMenuComponent: PropTypes.oneOfType([
|
||||
PropTypes.arrayOf(PropTypes.node),
|
||||
PropTypes.node,
|
||||
PropTypes.func,
|
||||
]).isRequired,
|
||||
onDelete: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default ContextMenuProvider;
|
||||
|
||||
export const ContextMenuConsumer = ContextMenuContext.Consumer;
|
||||
@@ -20,7 +20,30 @@ export const useDialog = () => useContext(DialogContext);
|
||||
const DialogProvider = ({ children, service }) => {
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [dialogs, setDialogs] = useState([]);
|
||||
const [lastDialogId, setLastDialogId] = useState(null);
|
||||
const [lastDialogPosition, setLastDialogPosition] = useState(null);
|
||||
const [centerPositions, setCenterPositions] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
setCenterPositions(
|
||||
dialogs.map(dialog => ({
|
||||
id: dialog.id,
|
||||
...getCenterPosition(dialog.id),
|
||||
}))
|
||||
);
|
||||
}, [dialogs]);
|
||||
|
||||
const getCenterPosition = id => {
|
||||
const root = document.querySelector('#root');
|
||||
const centerX = root.offsetLeft + root.offsetWidth / 2;
|
||||
const centerY = root.offsetTop + root.offsetHeight / 2;
|
||||
const item = document.querySelector(`#draggableItem-${id}`);
|
||||
const itemBounds = item.getBoundingClientRect();
|
||||
return {
|
||||
x: centerX - itemBounds.width / 2,
|
||||
y: centerY - itemBounds.height / 2,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets the implementation of a dialog service that can be used by extensions.
|
||||
@@ -42,13 +65,16 @@ const DialogProvider = ({ children, service }) => {
|
||||
* @property {Object} contentProps The dialog content props.
|
||||
* @property {boolean} isDraggable Controls if dialog content is draggable or not.
|
||||
* @property {boolean} showOverlay Controls dialog overlay.
|
||||
* @property {boolean} centralize Center the dialog on the screen.
|
||||
* @property {boolean} preservePosition Use last position instead of default.
|
||||
* @property {ElementPosition} defaultPosition Specifies the `x` and `y` that the dragged item should start at.
|
||||
* @property {ElementPosition} position If this property is present, the item becomes 'controlled' and is not responsive to user input.
|
||||
* @property {Function} onStart Called when dragging starts. If `false` is returned any handler, the action will cancel.
|
||||
* @property {Function} onStop Called when dragging stops.
|
||||
* @property {Function} onDrag Called while dragging.
|
||||
*/
|
||||
|
||||
useEffect(() => _bringToFront(lastDialogId), [_bringToFront, lastDialogId]);
|
||||
|
||||
/**
|
||||
* Creates a new dialog and return its id.
|
||||
*
|
||||
@@ -64,6 +90,7 @@ const DialogProvider = ({ children, service }) => {
|
||||
}
|
||||
|
||||
setDialogs(dialogs => [...dialogs, { ...props, id: dialogId }]);
|
||||
setLastDialogId(dialogId);
|
||||
|
||||
return dialogId;
|
||||
}, []);
|
||||
@@ -75,9 +102,11 @@ const DialogProvider = ({ children, service }) => {
|
||||
* @property {string} props.id The dialog id.
|
||||
* @returns void
|
||||
*/
|
||||
const dismiss = useCallback(({ id }) => {
|
||||
setDialogs(dialogs => dialogs.filter(dialog => dialog.id !== id));
|
||||
}, []);
|
||||
const dismiss = useCallback(
|
||||
({ id }) =>
|
||||
setDialogs(dialogs => dialogs.filter(dialog => dialog.id !== id)),
|
||||
[]
|
||||
);
|
||||
|
||||
/**
|
||||
* Dismisses all dialogs.
|
||||
@@ -101,14 +130,14 @@ const DialogProvider = ({ children, service }) => {
|
||||
* @param {string} id The dialog id.
|
||||
* @returns void
|
||||
*/
|
||||
const _bringToFront = id => {
|
||||
const _bringToFront = useCallback(id => {
|
||||
setDialogs(dialogs => {
|
||||
const topDialog = dialogs.find(dialog => dialog.id === id);
|
||||
return topDialog
|
||||
? [...dialogs.filter(dialog => dialog.id !== id), topDialog]
|
||||
: [];
|
||||
: dialogs;
|
||||
});
|
||||
};
|
||||
}, []);
|
||||
|
||||
const renderDialogs = () =>
|
||||
dialogs.map(dialog => {
|
||||
@@ -116,20 +145,27 @@ const DialogProvider = ({ children, service }) => {
|
||||
id,
|
||||
content: DialogContent,
|
||||
contentProps,
|
||||
position,
|
||||
defaultPosition,
|
||||
centralize = false,
|
||||
preservePosition = true,
|
||||
isDraggable = true,
|
||||
onStart,
|
||||
onStop,
|
||||
onDrag,
|
||||
} = dialog;
|
||||
|
||||
let position =
|
||||
(preservePosition && lastDialogPosition) || defaultPosition;
|
||||
if (centralize) {
|
||||
position = centerPositions.find(position => position.id === id);
|
||||
}
|
||||
|
||||
return (
|
||||
<Draggable
|
||||
key={id}
|
||||
disabled={!isDraggable}
|
||||
position={position}
|
||||
defaultPosition={lastDialogPosition || defaultPosition}
|
||||
defaultPosition={position}
|
||||
bounds="parent"
|
||||
onStart={event => {
|
||||
const e = event || window.event;
|
||||
@@ -169,7 +205,11 @@ const DialogProvider = ({ children, service }) => {
|
||||
>
|
||||
<div
|
||||
id={`draggableItem-${id}`}
|
||||
className={classNames('DraggableItem', isDragging && 'dragging')}
|
||||
className={classNames(
|
||||
'DraggableItem',
|
||||
isDragging && 'dragging',
|
||||
isDraggable && 'draggable'
|
||||
)}
|
||||
style={{ zIndex: '999', position: 'absolute' }}
|
||||
onClick={() => _bringToFront(id)}
|
||||
>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
.DraggableItem
|
||||
.DraggableItem.draggable
|
||||
div
|
||||
cursor: grab !important
|
||||
|
||||
.DraggableItem.dragging
|
||||
.DraggableItem.draggable.dragging
|
||||
div
|
||||
cursor: grabbing !important
|
||||
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
import React, {
|
||||
createContext,
|
||||
useContext,
|
||||
useEffect,
|
||||
useCallback,
|
||||
} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { useDialog } from './DialogProvider';
|
||||
|
||||
const LabellingFlowContext = createContext(null);
|
||||
const { Provider } = LabellingFlowContext;
|
||||
|
||||
export const useLabellingFlow = () => useContext(LabellingFlowContext);
|
||||
|
||||
const LabellingFlowProvider = ({
|
||||
children,
|
||||
service,
|
||||
labellingComponent: LabellingComponent,
|
||||
onUpdateLabelling,
|
||||
}) => {
|
||||
const { create, dismiss } = useDialog();
|
||||
|
||||
/**
|
||||
* Sets the implementation of a labelling flow service that can be used by extensions.
|
||||
*
|
||||
* @returns void
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (service) {
|
||||
service.setServiceImplementation({
|
||||
show,
|
||||
hide,
|
||||
});
|
||||
}
|
||||
}, [hide, service, show]);
|
||||
|
||||
const hide = useCallback(() => dismiss({ id: 'labelling' }), [dismiss]);
|
||||
|
||||
const show = useCallback(
|
||||
({ centralize, defaultPosition, props }) => {
|
||||
hide();
|
||||
create({
|
||||
id: 'labelling',
|
||||
centralize,
|
||||
isDraggable: false,
|
||||
showOverlay: true,
|
||||
content: LabellingComponent,
|
||||
defaultPosition,
|
||||
contentProps: {
|
||||
visible: true,
|
||||
measurementData: props.measurementData,
|
||||
labellingDoneCallback: () => dismiss({ id: 'labelling' }),
|
||||
updateLabelling: labellingData =>
|
||||
_updateLabellingHandler(labellingData, props.measurementData),
|
||||
...props,
|
||||
},
|
||||
});
|
||||
},
|
||||
[LabellingComponent, _updateLabellingHandler, create, dismiss, hide]
|
||||
);
|
||||
|
||||
const _updateLabellingHandler = useCallback(
|
||||
(labellingData, measurementData) => {
|
||||
const { location, description, response } = labellingData;
|
||||
|
||||
if (location) {
|
||||
measurementData.location = location;
|
||||
}
|
||||
|
||||
measurementData.description = description || '';
|
||||
|
||||
if (response) {
|
||||
measurementData.response = response;
|
||||
}
|
||||
|
||||
onUpdateLabelling(labellingData, measurementData);
|
||||
},
|
||||
[onUpdateLabelling]
|
||||
);
|
||||
|
||||
return (
|
||||
<Provider
|
||||
value={{
|
||||
show,
|
||||
hide,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</Provider>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Higher Order Component to use the labelling flow methods through a Class Component.
|
||||
*
|
||||
* @returns
|
||||
*/
|
||||
export const withLabellingFlow = Component => {
|
||||
return function WrappedComponent(props) {
|
||||
const { show, hide } = useLabellingFlow();
|
||||
return (
|
||||
<Component
|
||||
{...props}
|
||||
modal={{
|
||||
show,
|
||||
hide,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
LabellingFlowProvider.defaultProps = {
|
||||
service: null,
|
||||
};
|
||||
|
||||
LabellingFlowProvider.propTypes = {
|
||||
children: PropTypes.oneOfType([
|
||||
PropTypes.arrayOf(PropTypes.node),
|
||||
PropTypes.node,
|
||||
]).isRequired,
|
||||
service: PropTypes.shape({
|
||||
setServiceImplementation: PropTypes.func,
|
||||
}),
|
||||
labellingComponent: PropTypes.oneOfType([
|
||||
PropTypes.arrayOf(PropTypes.node),
|
||||
PropTypes.node,
|
||||
PropTypes.func,
|
||||
]).isRequired,
|
||||
onUpdateLabelling: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default LabellingFlowProvider;
|
||||
|
||||
export const LabellingFlowConsumer = LabellingFlowContext.Consumer;
|
||||
@@ -18,3 +18,15 @@ export {
|
||||
withDialog,
|
||||
useDialog,
|
||||
} from './DialogProvider.js';
|
||||
export {
|
||||
default as ContextMenuProvider,
|
||||
withContextMenu,
|
||||
useContextMenu,
|
||||
ContextMenuConsumer,
|
||||
} from './ContextMenuProvider.js';
|
||||
export {
|
||||
default as LabellingFlowProvider,
|
||||
withLabellingFlow,
|
||||
useLabellingFlow,
|
||||
LabellingFlowConsumer,
|
||||
} from './LabellingFlowProvider.js';
|
||||
@@ -62,6 +62,7 @@ import powerOff from './icons/power-off.svg';
|
||||
import reset from './icons/reset.svg';
|
||||
import rotate from './icons/rotate.svg';
|
||||
import rotateRight from './icons/rotate-right.svg';
|
||||
import saveRegular from './icons/save-regular.svg';
|
||||
import search from './icons/search.svg';
|
||||
import searchPlus from './icons/search-plus.svg';
|
||||
import softTissue from './icons/soft-tissue.svg';
|
||||
@@ -164,6 +165,7 @@ const ICONS = {
|
||||
'soft-tissue': softTissue,
|
||||
lung,
|
||||
liver,
|
||||
save: saveRegular,
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
focusable="false"
|
||||
data-icon="save"
|
||||
role="img" xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 448 512">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M433.941 129.941l-83.882-83.882A48 48 0 0 0 316.118 32H48C21.49 32 0 53.49 0 80v352c0 26.51 21.49 48 48 48h352c26.51 0 48-21.49 48-48V163.882a48 48 0 0 0-14.059-33.941zM272 80v80H144V80h128zm122 352H54a6 6 0 0 1-6-6V86a6 6 0 0 1 6-6h42v104c0 13.255 10.745 24 24 24h176c13.255 0 24-10.745 24-24V83.882l78.243 78.243a6 6 0 0 1 1.757 4.243V426a6 6 0 0 1-6 6zM224 232c-48.523 0-88 39.477-88 88s39.477 88 88 88 88-39.477 88-88-39.477-88-88-88zm0 128c-22.056 0-40-17.944-40-40s17.944-40 40-40 40 17.944 40 40-17.944 40-40 40z">
|
||||
</path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 738 B |
@@ -60,6 +60,14 @@ import {
|
||||
ModalConsumer,
|
||||
useModal,
|
||||
withModal,
|
||||
ContextMenuProvider,
|
||||
ContextMenuConsumer,
|
||||
useContextMenu,
|
||||
withContextMenu,
|
||||
LabellingFlowProvider,
|
||||
LabellingFlowConsumer,
|
||||
useLabellingFlow,
|
||||
withLabellingFlow,
|
||||
} from './contextProviders';
|
||||
|
||||
export {
|
||||
@@ -117,6 +125,14 @@ export {
|
||||
DialogProvider,
|
||||
withDialog,
|
||||
useDialog,
|
||||
ContextMenuProvider,
|
||||
ContextMenuConsumer,
|
||||
useContextMenu,
|
||||
withContextMenu,
|
||||
LabellingFlowProvider,
|
||||
LabellingFlowConsumer,
|
||||
useLabellingFlow,
|
||||
withLabellingFlow,
|
||||
// Hooks
|
||||
useDebounce,
|
||||
useMedia,
|
||||
|
||||
@@ -3,6 +3,87 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [3.1.4](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.1.3...@ohif/viewer@3.1.4) (2019-12-13)
|
||||
|
||||
**Note:** Version bump only for package @ohif/viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [3.1.3](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.1.2...@ohif/viewer@3.1.3) (2019-12-12)
|
||||
|
||||
**Note:** Version bump only for package @ohif/viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [3.1.2](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.1.1...@ohif/viewer@3.1.2) (2019-12-12)
|
||||
|
||||
**Note:** Version bump only for package @ohif/viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [3.1.1](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.1.0...@ohif/viewer@3.1.1) (2019-12-11)
|
||||
|
||||
**Note:** Version bump only for package @ohif/viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.1.0](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.0.3...@ohif/viewer@3.1.0) (2019-12-11)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* 🎸 DICOM SR STOW on MeasurementAPI ([#954](https://github.com/OHIF/Viewers/issues/954)) ([ebe1af8](https://github.com/OHIF/Viewers/commit/ebe1af8d4f75d2483eba869655906d7829bd9666)), closes [#758](https://github.com/OHIF/Viewers/issues/758)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [3.0.3](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.0.2...@ohif/viewer@3.0.3) (2019-12-11)
|
||||
|
||||
**Note:** Version bump only for package @ohif/viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [3.0.2](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.0.1...@ohif/viewer@3.0.2) (2019-12-11)
|
||||
|
||||
**Note:** Version bump only for package @ohif/viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [3.0.1](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.0.0...@ohif/viewer@3.0.1) (2019-12-09)
|
||||
|
||||
**Note:** Version bump only for package @ohif/viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.0.0](https://github.com/OHIF/Viewers/compare/@ohif/viewer@2.11.8...@ohif/viewer@3.0.0) (2019-12-09)
|
||||
|
||||
|
||||
* feat!: Ability to configure cornerstone tools via extension configuration (#1229) ([55a5806](https://github.com/OHIF/Viewers/commit/55a580659ecb74ca6433461d8f9a05c2a2b69533)), closes [#1229](https://github.com/OHIF/Viewers/issues/1229)
|
||||
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* modifies the exposed react <App /> components props. The contract for providing configuration for the app has changed. Please reference updated documentation for guidance.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.11.8](https://github.com/OHIF/Viewers/compare/@ohif/viewer@2.11.7...@ohif/viewer@2.11.8) (2019-12-07)
|
||||
|
||||
**Note:** Version bump only for package @ohif/viewer
|
||||
|
||||
@@ -81,10 +81,8 @@ describe('OHIF Cornerstone Hotkeys', () => {
|
||||
});
|
||||
|
||||
it('uses hotkeys "RightArrow" and "LeftArrow" to navigate between multiple viewports', () => {
|
||||
//Click on Layout button
|
||||
cy.get('@layoutBtn').click();
|
||||
//Select 3 viewports
|
||||
cy.get('tbody > :nth-child(1) > :nth-child(3)').click();
|
||||
//Select viewport layout (3,1)
|
||||
cy.setLayout(3, 1);
|
||||
cy.waitViewportImageLoading();
|
||||
|
||||
// Press multiples hotkeys on viewport #1
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
describe('OHIF Download Snapshot File', () => {
|
||||
before(() => {
|
||||
cy.openStudy('MISTER^MR');
|
||||
cy.waitDicomImage();
|
||||
cy.expectMinimumThumbnails(5);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
cy.openDownloadImageModal();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Close modal
|
||||
cy.get('[data-cy="close-button"]')
|
||||
.scrollIntoView()
|
||||
.click();
|
||||
});
|
||||
|
||||
it('checks displayed information for Tablet experience', function() {
|
||||
// Set Tablet resolution
|
||||
cy.viewport(1000, 660);
|
||||
// Visual comparison
|
||||
cy.screenshot('Download Image Modal - Tablet experience');
|
||||
cy.percyCanvasSnapshot('Download Image Modal - Tablet experience');
|
||||
});
|
||||
|
||||
it('checks displayed information for Desktop experience', function() {
|
||||
// Set Desktop resolution
|
||||
cy.viewport(1750, 720);
|
||||
// Visual comparison
|
||||
cy.screenshot('Download Image Modal - Desktop experience');
|
||||
cy.percyCanvasSnapshot('Download Image Modal - Desktop experience');
|
||||
//Check if all elements are displayed
|
||||
cy.get('[data-cy=modal-header]')
|
||||
.as('downloadImageModal')
|
||||
.should('contain.text', 'Download High Quality Image');
|
||||
// Check input fields
|
||||
cy.get('[data-cy=file-type]')
|
||||
.select('png')
|
||||
.should('have.value', 'png')
|
||||
.select('jpg')
|
||||
.should('have.value', 'jpg');
|
||||
// Check image preview
|
||||
cy.get('[data-cy="image-preview"]').should('contain.text', 'Image Preview');
|
||||
cy.get('[data-cy="viewport-preview-img"]')
|
||||
.should('have.attr', 'src')
|
||||
.and('include', 'data:image');
|
||||
// Check buttons
|
||||
cy.get('[data-cy="cancel-btn"]')
|
||||
.scrollIntoView()
|
||||
.should('be.visible');
|
||||
cy.get('[data-cy="download-btn"]')
|
||||
.scrollIntoView()
|
||||
.should('be.visible');
|
||||
});
|
||||
|
||||
it('cancel changes on download modal', function() {
|
||||
//Change Image Width, Filename and File Type
|
||||
cy.get('[data-cy="image-width"]')
|
||||
.clear()
|
||||
.type('300');
|
||||
cy.get('[data-cy="image-height"]') //Image Height should be the same as width
|
||||
.should('have.value', '300');
|
||||
cy.get('[data-cy="file-name"]')
|
||||
.clear()
|
||||
.type('new-filename');
|
||||
cy.get('[data-cy=file-type]').select('png');
|
||||
//Click on Cancel button
|
||||
cy.get('[data-cy="cancel-btn"]')
|
||||
.scrollIntoView()
|
||||
.click();
|
||||
//Check modal is closed
|
||||
cy.get('[data-cy="modal"]').should('not.exist');
|
||||
//Open Modal
|
||||
cy.openDownloadImageModal();
|
||||
//Verify default values was restored
|
||||
cy.get('[data-cy="image-width"]').should('have.value', '512');
|
||||
cy.get('[data-cy="file-name"]').should('have.value', 'image');
|
||||
cy.get('[data-cy=file-type]').should('have.value', 'jpg');
|
||||
});
|
||||
|
||||
// TO-DO once issue is fixed: https://github.com/OHIF/Viewers/issues/1217
|
||||
// it('checks error messages for empty fields', function() {
|
||||
// //Clear fields Image Width and Filename
|
||||
// cy.get('[data-cy="image-width"]')
|
||||
// .clear();
|
||||
// cy.get('[data-cy="file-name"]').clear();
|
||||
|
||||
// //Click on Download button
|
||||
// cy.get('[data-cy="download-btn"]')
|
||||
// .scrollIntoView()
|
||||
// .click();
|
||||
// //Check error message
|
||||
// });
|
||||
|
||||
it('checks if "Show Annotations" checkbox will display annotations', function() {
|
||||
// Close modal that is initially opened
|
||||
cy.get('[data-cy="close-button"]').click();
|
||||
|
||||
// Add measurements in the viewport
|
||||
cy.addLengthMeasurement();
|
||||
cy.addAngleMeasurement();
|
||||
|
||||
// Open Modal
|
||||
cy.openDownloadImageModal();
|
||||
// Select "Show Annotations" option
|
||||
cy.get('[data-cy="show-annotations"]').check();
|
||||
// Check image preview
|
||||
cy.get('[data-cy="image-preview"]').scrollIntoView();
|
||||
// Visual comparison
|
||||
cy.screenshot('Download Image Modal - Show Annotations checked');
|
||||
cy.percyCanvasSnapshot('Download Image Modal - Show Annotations checked');
|
||||
//Compare classes that exists on Image Preview with Annotations and Without Annotation
|
||||
cy.get('[data-cy="modal-content"]')
|
||||
.find('canvas')
|
||||
.should('have.class', 'magnifyTool'); //Class "MagnifyTool" exists with annotations displayed on Image preview
|
||||
// Uncheck "Show Annotations" option
|
||||
cy.get('[data-cy="show-annotations"]')
|
||||
.uncheck()
|
||||
.wait(300);
|
||||
// Check that class "MagnifyTool" should not exist
|
||||
cy.get('[data-cy="modal-content"]')
|
||||
.find('canvas')
|
||||
.should('not.have.class', 'magnifyTool');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
//We excluded the tests for '**/studies/**' because the bulk/all of our other study/viewer tests use this route
|
||||
|
||||
describe('OHIF Routes', function() {
|
||||
beforeEach(function() {
|
||||
cy.openStudyList();
|
||||
});
|
||||
|
||||
it('checks PT/CT json url study route', function() {
|
||||
cy.visit('/viewer?url=https://s3.eu-central-1.amazonaws.com/ohif-viewer/JSON/PTCTStudy.json');
|
||||
|
||||
cy.server();
|
||||
cy.route('GET', '**/PTCTStudy/**').as('getPTCTStudy');
|
||||
|
||||
cy.wait('@getPTCTStudy.all');
|
||||
cy.get('@getPTCTStudy').should($route => {
|
||||
expect($route.status).to.be.eq(200);
|
||||
});
|
||||
|
||||
cy.screenshot();
|
||||
cy.percyCanvasSnapshot('PT/CT json study route');
|
||||
});
|
||||
});
|
||||
@@ -1,24 +0,0 @@
|
||||
describe('ViewerRouting', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('/');
|
||||
cy.contains('Study List');
|
||||
cy.get(
|
||||
'[data-cy="study-list-results"]> :nth-child(1) > .patientId'
|
||||
).click();
|
||||
});
|
||||
|
||||
// it('thumbnails list has more than 2 items', () => {
|
||||
// cy.get('.scrollable-study-thumbnails div.thumbnail-container')
|
||||
// .its('length')
|
||||
// .should('be.gte', 2);
|
||||
// });
|
||||
|
||||
// it('loads route with at least 2 thumbnails', () => {
|
||||
// cy.get(
|
||||
// ':nth-child(1) > .ThumbnailEntry > .p-x-1 > .ImageThumbnail > .image-thumbnail-canvas > canvas'
|
||||
// ).should('be.visible');
|
||||
// cy.get(
|
||||
// ':nth-child(2) > .ThumbnailEntry > .p-x-1 > .ImageThumbnail > .image-thumbnail-canvas > canvas'
|
||||
// ).should('be.visible');
|
||||
// });
|
||||
});
|
||||
@@ -11,6 +11,7 @@ export function initCornerstoneToolsAliases() {
|
||||
cy.get('.ToolbarRow > :nth-child(10)').as('cineBtn');
|
||||
cy.get('.expandableToolMenu').as('moreBtn');
|
||||
cy.get('.btn-group > .toolbar-button').as('layoutBtn');
|
||||
cy.get('.viewport-element').as('viewport');
|
||||
}
|
||||
|
||||
//Creating aliases for Common page elements
|
||||
|
||||
@@ -469,6 +469,18 @@ Cypress.Commands.add(
|
||||
}
|
||||
);
|
||||
|
||||
Cypress.Commands.add('openDownloadImageModal', () => {
|
||||
// Click on More button
|
||||
cy.get('.expandableToolMenu')
|
||||
.as('moreBtn')
|
||||
.click();
|
||||
|
||||
// Click on Download button
|
||||
cy.get('.tooltip-inner > :nth-child(13)')
|
||||
.as('downloadBtn')
|
||||
.click();
|
||||
});
|
||||
|
||||
Cypress.Commands.add('setLanguage', (language, save = true) => {
|
||||
cy.openPreferences();
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/viewer",
|
||||
"version": "2.11.8",
|
||||
"version": "3.1.4",
|
||||
"description": "OHIF Viewer",
|
||||
"author": "OHIF Contributors",
|
||||
"license": "MIT",
|
||||
@@ -27,7 +27,7 @@
|
||||
"dev:viewer": "yarn run dev",
|
||||
"start": "yarn run dev",
|
||||
"test:e2e": "cypress open",
|
||||
"test:e2e:ci": "percy exec -- cypress run --browser chrome",
|
||||
"test:e2e:ci": "percy exec -- cypress run --record --browser chrome",
|
||||
"test:e2e:dist": "start-server-and-test test:e2e:serve http://localhost:3000 test:e2e:ci",
|
||||
"test:e2e:serve": "serve -n -l 3000 -s dist",
|
||||
"test:unit": "jest --watchAll",
|
||||
@@ -45,14 +45,14 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.5.5",
|
||||
"@ohif/core": "^1.13.3",
|
||||
"@ohif/extension-cornerstone": "^2.0.0",
|
||||
"@ohif/extension-dicom-html": "^1.0.2",
|
||||
"@ohif/core": "^2.1.0",
|
||||
"@ohif/extension-cornerstone": "^2.1.0",
|
||||
"@ohif/extension-dicom-html": "^1.1.0",
|
||||
"@ohif/extension-dicom-microscopy": "^0.50.6",
|
||||
"@ohif/extension-dicom-pdf": "^1.0.0",
|
||||
"@ohif/extension-vtk": "^0.54.6",
|
||||
"@ohif/i18n": "^0.52.2",
|
||||
"@ohif/ui": "^0.65.4",
|
||||
"@ohif/extension-vtk": "^1.1.2",
|
||||
"@ohif/i18n": "^0.52.3",
|
||||
"@ohif/ui": "^1.1.2",
|
||||
"@tanem/react-nprogress": "^1.1.25",
|
||||
"classnames": "^2.2.6",
|
||||
"core-js": "^3.2.1",
|
||||
@@ -60,7 +60,7 @@
|
||||
"cornerstone-math": "^0.1.8",
|
||||
"cornerstone-tools": "^4.8.0",
|
||||
"cornerstone-wado-image-loader": "^3.0.0",
|
||||
"dcmjs": "^0.6.1",
|
||||
"dcmjs": "^0.8.2",
|
||||
"dicom-parser": "^1.8.3",
|
||||
"dicomweb-client": "^0.4.4",
|
||||
"hammerjs": "^2.0.8",
|
||||
@@ -85,7 +85,7 @@
|
||||
"vtk.js": "^11.7.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"cypress": "^3.3.1",
|
||||
"cypress": "^3.8.0",
|
||||
"gh-pages": "2.0.1",
|
||||
"identity-obj-proxy": "3.0.x",
|
||||
"lodash": "4.17.11",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
window.config = {
|
||||
// default: '/'
|
||||
routerBasename: '/',
|
||||
whiteLabelling: {},
|
||||
extensions: [],
|
||||
showStudyList: true,
|
||||
filterQueryParam: false,
|
||||
@@ -69,4 +70,5 @@ window.config = {
|
||||
// ~ Cornerstone Tools
|
||||
{ commandName: 'setZoomTool', label: 'Zoom', keys: ['z'] },
|
||||
],
|
||||
cornerstoneExtensionConfig: {},
|
||||
};
|
||||
@@ -1,5 +1,6 @@
|
||||
window.config = {
|
||||
routerBasename: '/',
|
||||
whiteLabelling: {},
|
||||
extensions: [],
|
||||
showStudyList: true,
|
||||
servers: {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
window.config = {
|
||||
routerBasename: '/',
|
||||
whiteLabelling: {},
|
||||
showStudyList: true,
|
||||
servers: {
|
||||
dicomWeb: [
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
window.config = {
|
||||
routerBasename: '/',
|
||||
whiteLabelling: {},
|
||||
showStudyList: true,
|
||||
servers: {
|
||||
// This is an array, but we'll only use the first entry for now
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
window.config = {
|
||||
routerBasename: '/',
|
||||
whiteLabelling: {},
|
||||
showStudyList: true,
|
||||
servers: {
|
||||
// This is an array, but we'll only use the first entry for now
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
window.config = {
|
||||
routerBasename: '/',
|
||||
whiteLabelling: {},
|
||||
enableGoogleCloudAdapter: true,
|
||||
servers: {
|
||||
// This is an array, but we'll only use the first entry for now
|
||||
@@ -14,7 +15,8 @@ window.config = {
|
||||
client_id: 'YOURCLIENTID.apps.googleusercontent.com',
|
||||
redirect_uri: '/callback', // `OHIFStandaloneViewer.js`
|
||||
response_type: 'id_token token',
|
||||
scope: 'email profile openid https://www.googleapis.com/auth/cloudplatformprojects.readonly https://www.googleapis.com/auth/cloud-healthcare', // email profile openid
|
||||
scope:
|
||||
'email profile openid https://www.googleapis.com/auth/cloudplatformprojects.readonly https://www.googleapis.com/auth/cloud-healthcare', // email profile openid
|
||||
// ~ OPTIONAL
|
||||
post_logout_redirect_uri: '/logout-redirect.html',
|
||||
revoke_uri: 'https://accounts.google.com/o/oauth2/revoke?token=',
|
||||
@@ -23,4 +25,4 @@ window.config = {
|
||||
},
|
||||
],
|
||||
studyListFunctionsEnabled: true,
|
||||
}
|
||||
};
|
||||
@@ -1,6 +1,7 @@
|
||||
window.config = {
|
||||
// default: '/'
|
||||
routerBasename: '/',
|
||||
whiteLabelling: {},
|
||||
// default: ''
|
||||
showStudyList: true,
|
||||
servers: {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
window.config = {
|
||||
routerBasename: '/pwa',
|
||||
whiteLabelling: {},
|
||||
showStudyList: true,
|
||||
servers: {
|
||||
dicomWeb: [
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
window.config = {
|
||||
routerBasename: '/',
|
||||
whiteLabelling: {},
|
||||
showStudyList: true,
|
||||
servers: {
|
||||
dicomWeb: [
|
||||
|
||||
@@ -1,74 +1,102 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<meta name="application-name" content="OHIF Viewer">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||
<meta name="apple-mobile-web-app-title" content="@ohif/viewer">
|
||||
<meta name="msapplication-TileColor" content="#fff">
|
||||
<meta name="msapplication-TileImage" content="<%= PUBLIC_URL %>assets/mstile-144x144.png">
|
||||
<meta name="msapplication-config" content="<%= PUBLIC_URL %>assets/browserconfig.xml">
|
||||
<link rel="manifest" href="<%= PUBLIC_URL %>manifest.json">
|
||||
|
||||
<link rel="shortcut icon" href="<%= PUBLIC_URL %>assets/favicon.ico">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="<%= PUBLIC_URL %>assets/favicon-16x16.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="<%= PUBLIC_URL %>assets/favicon-32x32.png">
|
||||
<link rel="apple-touch-icon" sizes="57x57" href="<%= PUBLIC_URL %>assets/apple-touch-icon-57x57.png">
|
||||
<link rel="apple-touch-icon" sizes="60x60" href="<%= PUBLIC_URL %>assets/apple-touch-icon-60x60.png">
|
||||
<link rel="apple-touch-icon" sizes="72x72" href="<%= PUBLIC_URL %>assets/apple-touch-icon-72x72.png">
|
||||
<link rel="apple-touch-icon" sizes="76x76" href="<%= PUBLIC_URL %>assets/apple-touch-icon-76x76.png">
|
||||
<link rel="apple-touch-icon" sizes="114x114" href="<%= PUBLIC_URL %>assets/apple-touch-icon-114x114.png">
|
||||
<link rel="apple-touch-icon" sizes="120x120" href="<%= PUBLIC_URL %>assets/apple-touch-icon-120x120.png">
|
||||
<link rel="apple-touch-icon" sizes="144x144" href="<%= PUBLIC_URL %>assets/apple-touch-icon-144x144.png">
|
||||
<link rel="apple-touch-icon" sizes="152x152" href="<%= PUBLIC_URL %>assets/apple-touch-icon-152x152.png">
|
||||
<link rel="apple-touch-icon" sizes="167x167" href="<%= PUBLIC_URL %>assets/apple-touch-icon-167x167.png">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="<%= PUBLIC_URL %>assets/apple-touch-icon-180x180.png">
|
||||
<link rel="apple-touch-icon" sizes="1024x1024" href="<%= PUBLIC_URL %>assets/apple-touch-icon-1024x1024.png">
|
||||
<link rel="apple-touch-startup-image" media="(device-width: 320px) and (device-height: 480px) and (-webkit-device-pixel-ratio: 1)" href="<%= PUBLIC_URL %>assets/apple-touch-startup-image-320x460.png">
|
||||
<link rel="apple-touch-startup-image" media="(device-width: 320px) and (device-height: 480px) and (-webkit-device-pixel-ratio: 2)" href="<%= PUBLIC_URL %>assets/apple-touch-startup-image-640x920.png">
|
||||
<link rel="apple-touch-startup-image" media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2)" href="<%= PUBLIC_URL %>assets/apple-touch-startup-image-640x1096.png">
|
||||
<link rel="apple-touch-startup-image" media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2)" href="<%= PUBLIC_URL %>assets/apple-touch-startup-image-750x1294.png">
|
||||
<link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 736px) and (orientation: landscape) and (-webkit-device-pixel-ratio: 3)" href="<%= PUBLIC_URL %>assets/apple-touch-startup-image-1182x2208.png">
|
||||
<link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 736px) and (orientation: portrait) and (-webkit-device-pixel-ratio: 3)" href="<%= PUBLIC_URL %>assets/apple-touch-startup-image-1242x2148.png">
|
||||
<link rel="apple-touch-startup-image" media="(device-width: 768px) and (device-height: 1024px) and (orientation: landscape) and (-webkit-device-pixel-ratio: 1)" href="<%= PUBLIC_URL %>assets/apple-touch-startup-image-748x1024.png">
|
||||
<link rel="apple-touch-startup-image" media="(device-width: 768px) and (device-height: 1024px) and (orientation: portrait) and (-webkit-device-pixel-ratio: 1)" href="<%= PUBLIC_URL %>assets/apple-touch-startup-image-768x1004.png">
|
||||
<link rel="apple-touch-startup-image" media="(device-width: 768px) and (device-height: 1024px) and (orientation: landscape) and (-webkit-device-pixel-ratio: 2)" href="<%= PUBLIC_URL %>assets/apple-touch-startup-image-1496x2048.png">
|
||||
<link rel="apple-touch-startup-image" media="(device-width: 768px) and (device-height: 1024px) and (orientation: portrait) and (-webkit-device-pixel-ratio: 2)" href="<%= PUBLIC_URL %>assets/apple-touch-startup-image-1536x2008.png">
|
||||
<link rel="icon" type="image/png" sizes="228x228" href="<%= PUBLIC_URL %>assets/coast-228x228.png">
|
||||
<link rel="yandex-tableau-widget" href="<%= PUBLIC_URL %>assets/yandex-browser-manifest.json">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<meta name="application-name" content="OHIF Viewer">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||
<meta name="apple-mobile-web-app-title" content="@ohif/viewer">
|
||||
<meta name="msapplication-TileColor" content="#fff">
|
||||
<meta name="msapplication-TileImage" content="<%= PUBLIC_URL %>assets/mstile-144x144.png">
|
||||
<meta name="msapplication-config" content="<%= PUBLIC_URL %>assets/browserconfig.xml">
|
||||
<link rel="manifest" href="<%= PUBLIC_URL %>manifest.json">
|
||||
|
||||
<!-- Built with: https://polyfill.io/v3/url-builder/ -->
|
||||
<!-- Targets IE11 -->
|
||||
<script src="https://polyfill.io/v3/polyfill.min.js?flags=gated&features=default%2CObject.values%2CArray.prototype.flat%2CObject.entries%2CSymbol%2CArray.prototype.includes%2CString.prototype.repeat%2CArray.prototype.find"></script>
|
||||
<script type="text/javascript">
|
||||
window.PUBLIC_URL = '<%= PUBLIC_URL %>';
|
||||
</script>
|
||||
<script type="text/javascript" src="<%= PUBLIC_URL %>app-config.js"></script>
|
||||
<script type="module" src="<%= PUBLIC_URL %>init-service-worker.js"></script>
|
||||
<link rel="shortcut icon" href="<%= PUBLIC_URL %>assets/favicon.ico">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="<%= PUBLIC_URL %>assets/favicon-16x16.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="<%= PUBLIC_URL %>assets/favicon-32x32.png">
|
||||
<link rel="apple-touch-icon" sizes="57x57" href="<%= PUBLIC_URL %>assets/apple-touch-icon-57x57.png">
|
||||
<link rel="apple-touch-icon" sizes="60x60" href="<%= PUBLIC_URL %>assets/apple-touch-icon-60x60.png">
|
||||
<link rel="apple-touch-icon" sizes="72x72" href="<%= PUBLIC_URL %>assets/apple-touch-icon-72x72.png">
|
||||
<link rel="apple-touch-icon" sizes="76x76" href="<%= PUBLIC_URL %>assets/apple-touch-icon-76x76.png">
|
||||
<link rel="apple-touch-icon" sizes="114x114" href="<%= PUBLIC_URL %>assets/apple-touch-icon-114x114.png">
|
||||
<link rel="apple-touch-icon" sizes="120x120" href="<%= PUBLIC_URL %>assets/apple-touch-icon-120x120.png">
|
||||
<link rel="apple-touch-icon" sizes="144x144" href="<%= PUBLIC_URL %>assets/apple-touch-icon-144x144.png">
|
||||
<link rel="apple-touch-icon" sizes="152x152" href="<%= PUBLIC_URL %>assets/apple-touch-icon-152x152.png">
|
||||
<link rel="apple-touch-icon" sizes="167x167" href="<%= PUBLIC_URL %>assets/apple-touch-icon-167x167.png">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="<%= PUBLIC_URL %>assets/apple-touch-icon-180x180.png">
|
||||
<link rel="apple-touch-icon" sizes="1024x1024" href="<%= PUBLIC_URL %>assets/apple-touch-icon-1024x1024.png">
|
||||
<link rel="apple-touch-startup-image"
|
||||
media="(device-width: 320px) and (device-height: 480px) and (-webkit-device-pixel-ratio: 1)"
|
||||
href="<%= PUBLIC_URL %>assets/apple-touch-startup-image-320x460.png">
|
||||
<link rel="apple-touch-startup-image"
|
||||
media="(device-width: 320px) and (device-height: 480px) and (-webkit-device-pixel-ratio: 2)"
|
||||
href="<%= PUBLIC_URL %>assets/apple-touch-startup-image-640x920.png">
|
||||
<link rel="apple-touch-startup-image"
|
||||
media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2)"
|
||||
href="<%= PUBLIC_URL %>assets/apple-touch-startup-image-640x1096.png">
|
||||
<link rel="apple-touch-startup-image"
|
||||
media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2)"
|
||||
href="<%= PUBLIC_URL %>assets/apple-touch-startup-image-750x1294.png">
|
||||
<link rel="apple-touch-startup-image"
|
||||
media="(device-width: 414px) and (device-height: 736px) and (orientation: landscape) and (-webkit-device-pixel-ratio: 3)"
|
||||
href="<%= PUBLIC_URL %>assets/apple-touch-startup-image-1182x2208.png">
|
||||
<link rel="apple-touch-startup-image"
|
||||
media="(device-width: 414px) and (device-height: 736px) and (orientation: portrait) and (-webkit-device-pixel-ratio: 3)"
|
||||
href="<%= PUBLIC_URL %>assets/apple-touch-startup-image-1242x2148.png">
|
||||
<link rel="apple-touch-startup-image"
|
||||
media="(device-width: 768px) and (device-height: 1024px) and (orientation: landscape) and (-webkit-device-pixel-ratio: 1)"
|
||||
href="<%= PUBLIC_URL %>assets/apple-touch-startup-image-748x1024.png">
|
||||
<link rel="apple-touch-startup-image"
|
||||
media="(device-width: 768px) and (device-height: 1024px) and (orientation: portrait) and (-webkit-device-pixel-ratio: 1)"
|
||||
href="<%= PUBLIC_URL %>assets/apple-touch-startup-image-768x1004.png">
|
||||
<link rel="apple-touch-startup-image"
|
||||
media="(device-width: 768px) and (device-height: 1024px) and (orientation: landscape) and (-webkit-device-pixel-ratio: 2)"
|
||||
href="<%= PUBLIC_URL %>assets/apple-touch-startup-image-1496x2048.png">
|
||||
<link rel="apple-touch-startup-image"
|
||||
media="(device-width: 768px) and (device-height: 1024px) and (orientation: portrait) and (-webkit-device-pixel-ratio: 2)"
|
||||
href="<%= PUBLIC_URL %>assets/apple-touch-startup-image-1536x2008.png">
|
||||
<link rel="icon" type="image/png" sizes="228x228" href="<%= PUBLIC_URL %>assets/coast-228x228.png">
|
||||
<link rel="yandex-tableau-widget" href="<%= PUBLIC_URL %>assets/yandex-browser-manifest.json">
|
||||
|
||||
<title>OHIF Viewer</title>
|
||||
<!-- Built with: https://polyfill.io/v3/url-builder/ -->
|
||||
<!-- Targets IE11 -->
|
||||
<script
|
||||
src="https://polyfill.io/v3/polyfill.min.js?flags=gated&features=default%2CObject.values%2CArray.prototype.flat%2CObject.entries%2CSymbol%2CArray.prototype.includes%2CString.prototype.repeat%2CArray.prototype.find"></script>
|
||||
<script type="text/javascript">
|
||||
window.PUBLIC_URL = '<%= PUBLIC_URL %>';
|
||||
</script>
|
||||
<script type="text/javascript" src="<%= PUBLIC_URL %>app-config.js"></script>
|
||||
<script type="module" src="<%= PUBLIC_URL %>init-service-worker.js"></script>
|
||||
|
||||
<!-- WEB FONTS -->
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<title>OHIF Viewer</title>
|
||||
|
||||
<!-- EXTENSIONS -->
|
||||
<!-- <script type="text/javascript" src="path/to/some-extension.js"></script>
|
||||
<!-- WEB FONTS -->
|
||||
<link href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700&display=swap" rel="stylesheet" />
|
||||
|
||||
<!-- EXTENSIONS -->
|
||||
<!-- <script type="text/javascript" src="path/to/some-extension.js"></script>
|
||||
|
||||
<script>
|
||||
// If configuration file is a JS Object:
|
||||
window.config.extensions = [SomeExtension];
|
||||
|
||||
// If configuration file is a JS Function:
|
||||
window.config = () => {
|
||||
return {
|
||||
extensions: [SomeExtension]
|
||||
};
|
||||
};
|
||||
</script> -->
|
||||
</head>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<noscript> You need to enable JavaScript to run this app. </noscript>
|
||||
<body>
|
||||
<noscript> You need to enable JavaScript to run this app. </noscript>
|
||||
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,86 +1,117 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<meta name="application-name" content="OHIF Viewer">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||
<meta name="apple-mobile-web-app-title" content="@ohif/viewer">
|
||||
<meta name="msapplication-TileColor" content="#fff">
|
||||
<meta name="msapplication-TileImage" content="<%= PUBLIC_URL %>assets/mstile-144x144.png">
|
||||
<meta name="msapplication-config" content="<%= PUBLIC_URL %>assets/browserconfig.xml">
|
||||
<link rel="manifest" href="<%= PUBLIC_URL %>manifest.json">
|
||||
|
||||
<link rel="shortcut icon" href="<%= PUBLIC_URL %>assets/favicon.ico">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="<%= PUBLIC_URL %>assets/favicon-16x16.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="<%= PUBLIC_URL %>assets/favicon-32x32.png">
|
||||
<link rel="apple-touch-icon" sizes="57x57" href="<%= PUBLIC_URL %>assets/apple-touch-icon-57x57.png">
|
||||
<link rel="apple-touch-icon" sizes="60x60" href="<%= PUBLIC_URL %>assets/apple-touch-icon-60x60.png">
|
||||
<link rel="apple-touch-icon" sizes="72x72" href="<%= PUBLIC_URL %>assets/apple-touch-icon-72x72.png">
|
||||
<link rel="apple-touch-icon" sizes="76x76" href="<%= PUBLIC_URL %>assets/apple-touch-icon-76x76.png">
|
||||
<link rel="apple-touch-icon" sizes="114x114" href="<%= PUBLIC_URL %>assets/apple-touch-icon-114x114.png">
|
||||
<link rel="apple-touch-icon" sizes="120x120" href="<%= PUBLIC_URL %>assets/apple-touch-icon-120x120.png">
|
||||
<link rel="apple-touch-icon" sizes="144x144" href="<%= PUBLIC_URL %>assets/apple-touch-icon-144x144.png">
|
||||
<link rel="apple-touch-icon" sizes="152x152" href="<%= PUBLIC_URL %>assets/apple-touch-icon-152x152.png">
|
||||
<link rel="apple-touch-icon" sizes="167x167" href="<%= PUBLIC_URL %>assets/apple-touch-icon-167x167.png">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="<%= PUBLIC_URL %>assets/apple-touch-icon-180x180.png">
|
||||
<link rel="apple-touch-icon" sizes="1024x1024" href="<%= PUBLIC_URL %>assets/apple-touch-icon-1024x1024.png">
|
||||
<link rel="apple-touch-startup-image" media="(device-width: 320px) and (device-height: 480px) and (-webkit-device-pixel-ratio: 1)" href="<%= PUBLIC_URL %>assets/apple-touch-startup-image-320x460.png">
|
||||
<link rel="apple-touch-startup-image" media="(device-width: 320px) and (device-height: 480px) and (-webkit-device-pixel-ratio: 2)" href="<%= PUBLIC_URL %>assets/apple-touch-startup-image-640x920.png">
|
||||
<link rel="apple-touch-startup-image" media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2)" href="<%= PUBLIC_URL %>assets/apple-touch-startup-image-640x1096.png">
|
||||
<link rel="apple-touch-startup-image" media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2)" href="<%= PUBLIC_URL %>assets/apple-touch-startup-image-750x1294.png">
|
||||
<link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 736px) and (orientation: landscape) and (-webkit-device-pixel-ratio: 3)" href="<%= PUBLIC_URL %>assets/apple-touch-startup-image-1182x2208.png">
|
||||
<link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 736px) and (orientation: portrait) and (-webkit-device-pixel-ratio: 3)" href="<%= PUBLIC_URL %>assets/apple-touch-startup-image-1242x2148.png">
|
||||
<link rel="apple-touch-startup-image" media="(device-width: 768px) and (device-height: 1024px) and (orientation: landscape) and (-webkit-device-pixel-ratio: 1)" href="<%= PUBLIC_URL %>assets/apple-touch-startup-image-748x1024.png">
|
||||
<link rel="apple-touch-startup-image" media="(device-width: 768px) and (device-height: 1024px) and (orientation: portrait) and (-webkit-device-pixel-ratio: 1)" href="<%= PUBLIC_URL %>assets/apple-touch-startup-image-768x1004.png">
|
||||
<link rel="apple-touch-startup-image" media="(device-width: 768px) and (device-height: 1024px) and (orientation: landscape) and (-webkit-device-pixel-ratio: 2)" href="<%= PUBLIC_URL %>assets/apple-touch-startup-image-1496x2048.png">
|
||||
<link rel="apple-touch-startup-image" media="(device-width: 768px) and (device-height: 1024px) and (orientation: portrait) and (-webkit-device-pixel-ratio: 2)" href="<%= PUBLIC_URL %>assets/apple-touch-startup-image-1536x2008.png">
|
||||
<link rel="icon" type="image/png" sizes="228x228" href="<%= PUBLIC_URL %>assets/coast-228x228.png">
|
||||
<link rel="yandex-tableau-widget" href="<%= PUBLIC_URL %>assets/yandex-browser-manifest.json">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<meta name="application-name" content="OHIF Viewer">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||
<meta name="apple-mobile-web-app-title" content="@ohif/viewer">
|
||||
<meta name="msapplication-TileColor" content="#fff">
|
||||
<meta name="msapplication-TileImage" content="<%= PUBLIC_URL %>assets/mstile-144x144.png">
|
||||
<meta name="msapplication-config" content="<%= PUBLIC_URL %>assets/browserconfig.xml">
|
||||
<link rel="manifest" href="<%= PUBLIC_URL %>manifest.json">
|
||||
|
||||
<!-- Built with: https://polyfill.io/v3/url-builder/ -->
|
||||
<!-- Targets IE11 -->
|
||||
<script src="https://polyfill.io/v3/polyfill.min.js?flags=gated&features=default%2CObject.values%2CArray.prototype.flat%2CObject.entries%2CSymbol%2CArray.prototype.includes%2CString.prototype.repeat%2CArray.prototype.find"></script>
|
||||
<script type="text/javascript">
|
||||
window.PUBLIC_URL = '<%= PUBLIC_URL %>';
|
||||
</script>
|
||||
<script type="text/javascript" src="<%= PUBLIC_URL %>app-config.js"></script>
|
||||
<script type="module" src="<%= PUBLIC_URL %>init-service-worker.js"></script>
|
||||
<link rel="shortcut icon" href="<%= PUBLIC_URL %>assets/favicon.ico">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="<%= PUBLIC_URL %>assets/favicon-16x16.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="<%= PUBLIC_URL %>assets/favicon-32x32.png">
|
||||
<link rel="apple-touch-icon" sizes="57x57" href="<%= PUBLIC_URL %>assets/apple-touch-icon-57x57.png">
|
||||
<link rel="apple-touch-icon" sizes="60x60" href="<%= PUBLIC_URL %>assets/apple-touch-icon-60x60.png">
|
||||
<link rel="apple-touch-icon" sizes="72x72" href="<%= PUBLIC_URL %>assets/apple-touch-icon-72x72.png">
|
||||
<link rel="apple-touch-icon" sizes="76x76" href="<%= PUBLIC_URL %>assets/apple-touch-icon-76x76.png">
|
||||
<link rel="apple-touch-icon" sizes="114x114" href="<%= PUBLIC_URL %>assets/apple-touch-icon-114x114.png">
|
||||
<link rel="apple-touch-icon" sizes="120x120" href="<%= PUBLIC_URL %>assets/apple-touch-icon-120x120.png">
|
||||
<link rel="apple-touch-icon" sizes="144x144" href="<%= PUBLIC_URL %>assets/apple-touch-icon-144x144.png">
|
||||
<link rel="apple-touch-icon" sizes="152x152" href="<%= PUBLIC_URL %>assets/apple-touch-icon-152x152.png">
|
||||
<link rel="apple-touch-icon" sizes="167x167" href="<%= PUBLIC_URL %>assets/apple-touch-icon-167x167.png">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="<%= PUBLIC_URL %>assets/apple-touch-icon-180x180.png">
|
||||
<link rel="apple-touch-icon" sizes="1024x1024" href="<%= PUBLIC_URL %>assets/apple-touch-icon-1024x1024.png">
|
||||
<link rel="apple-touch-startup-image"
|
||||
media="(device-width: 320px) and (device-height: 480px) and (-webkit-device-pixel-ratio: 1)"
|
||||
href="<%= PUBLIC_URL %>assets/apple-touch-startup-image-320x460.png">
|
||||
<link rel="apple-touch-startup-image"
|
||||
media="(device-width: 320px) and (device-height: 480px) and (-webkit-device-pixel-ratio: 2)"
|
||||
href="<%= PUBLIC_URL %>assets/apple-touch-startup-image-640x920.png">
|
||||
<link rel="apple-touch-startup-image"
|
||||
media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2)"
|
||||
href="<%= PUBLIC_URL %>assets/apple-touch-startup-image-640x1096.png">
|
||||
<link rel="apple-touch-startup-image"
|
||||
media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2)"
|
||||
href="<%= PUBLIC_URL %>assets/apple-touch-startup-image-750x1294.png">
|
||||
<link rel="apple-touch-startup-image"
|
||||
media="(device-width: 414px) and (device-height: 736px) and (orientation: landscape) and (-webkit-device-pixel-ratio: 3)"
|
||||
href="<%= PUBLIC_URL %>assets/apple-touch-startup-image-1182x2208.png">
|
||||
<link rel="apple-touch-startup-image"
|
||||
media="(device-width: 414px) and (device-height: 736px) and (orientation: portrait) and (-webkit-device-pixel-ratio: 3)"
|
||||
href="<%= PUBLIC_URL %>assets/apple-touch-startup-image-1242x2148.png">
|
||||
<link rel="apple-touch-startup-image"
|
||||
media="(device-width: 768px) and (device-height: 1024px) and (orientation: landscape) and (-webkit-device-pixel-ratio: 1)"
|
||||
href="<%= PUBLIC_URL %>assets/apple-touch-startup-image-748x1024.png">
|
||||
<link rel="apple-touch-startup-image"
|
||||
media="(device-width: 768px) and (device-height: 1024px) and (orientation: portrait) and (-webkit-device-pixel-ratio: 1)"
|
||||
href="<%= PUBLIC_URL %>assets/apple-touch-startup-image-768x1004.png">
|
||||
<link rel="apple-touch-startup-image"
|
||||
media="(device-width: 768px) and (device-height: 1024px) and (orientation: landscape) and (-webkit-device-pixel-ratio: 2)"
|
||||
href="<%= PUBLIC_URL %>assets/apple-touch-startup-image-1496x2048.png">
|
||||
<link rel="apple-touch-startup-image"
|
||||
media="(device-width: 768px) and (device-height: 1024px) and (orientation: portrait) and (-webkit-device-pixel-ratio: 2)"
|
||||
href="<%= PUBLIC_URL %>assets/apple-touch-startup-image-1536x2008.png">
|
||||
<link rel="icon" type="image/png" sizes="228x228" href="<%= PUBLIC_URL %>assets/coast-228x228.png">
|
||||
<link rel="yandex-tableau-widget" href="<%= PUBLIC_URL %>assets/yandex-browser-manifest.json">
|
||||
|
||||
<title>OHIF Viewer</title>
|
||||
<!-- Built with: https://polyfill.io/v3/url-builder/ -->
|
||||
<!-- Targets IE11 -->
|
||||
<script
|
||||
src="https://polyfill.io/v3/polyfill.min.js?flags=gated&features=default%2CObject.values%2CArray.prototype.flat%2CObject.entries%2CSymbol%2CArray.prototype.includes%2CString.prototype.repeat%2CArray.prototype.find"></script>
|
||||
<script type="text/javascript">
|
||||
window.PUBLIC_URL = '<%= PUBLIC_URL %>';
|
||||
</script>
|
||||
<script type="text/javascript" src="<%= PUBLIC_URL %>app-config.js"></script>
|
||||
<script type="module" src="<%= PUBLIC_URL %>init-service-worker.js"></script>
|
||||
|
||||
<!-- WEB FONTS -->
|
||||
<link href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700&display=swap" rel="stylesheet" />
|
||||
<title>OHIF Viewer</title>
|
||||
|
||||
<!-- EXTENSIONS -->
|
||||
<!-- <script type="text/javascript" src="path/to/some-extension.js"></script>
|
||||
<!-- WEB FONTS -->
|
||||
<link href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700&display=swap" rel="stylesheet" />
|
||||
|
||||
<!-- EXTENSIONS -->
|
||||
<!-- <script type="text/javascript" src="path/to/some-extension.js"></script>
|
||||
|
||||
<script>
|
||||
// If configuration file is a JS Object:
|
||||
window.config.extensions = [SomeExtension];
|
||||
|
||||
// If configuration file is a JS Function:
|
||||
window.config = () => {
|
||||
return {
|
||||
extensions: [SomeExtension]
|
||||
};
|
||||
};
|
||||
</script> -->
|
||||
</head>
|
||||
<body>
|
||||
<noscript> You need to enable JavaScript to run this app. </noscript>
|
||||
</head>
|
||||
|
||||
<div id="root"></div>
|
||||
<body>
|
||||
<noscript> You need to enable JavaScript to run this app. </noscript>
|
||||
|
||||
<script>
|
||||
// Note: This is a client access token so there is no issue with having it publicly available
|
||||
var _rollbarConfig = {
|
||||
accessToken: "739c64cd39e84592ba062bc29aac9769",
|
||||
captureUncaught: true,
|
||||
captureUnhandledRejections: true,
|
||||
payload: {
|
||||
environment: "viewer.ohif.org"
|
||||
}
|
||||
};
|
||||
// Rollbar Snippet
|
||||
!function (r) { var e = {}; function o(n) { if (e[n]) return e[n].exports; var t = e[n] = { i: n, l: !1, exports: {} }; return r[n].call(t.exports, t, t.exports, o), t.l = !0, t.exports } o.m = r, o.c = e, o.d = function (r, e, n) { o.o(r, e) || Object.defineProperty(r, e, { enumerable: !0, get: n }) }, o.r = function (r) { "undefined" != typeof Symbol && Symbol.toStringTag && Object.defineProperty(r, Symbol.toStringTag, { value: "Module" }), Object.defineProperty(r, "__esModule", { value: !0 }) }, o.t = function (r, e) { if (1 & e && (r = o(r)), 8 & e) return r; if (4 & e && "object" == typeof r && r && r.__esModule) return r; var n = Object.create(null); if (o.r(n), Object.defineProperty(n, "default", { enumerable: !0, value: r }), 2 & e && "string" != typeof r) for (var t in r) o.d(n, t, function (e) { return r[e] }.bind(null, t)); return n }, o.n = function (r) { var e = r && r.__esModule ? function () { return r.default } : function () { return r }; return o.d(e, "a", e), e }, o.o = function (r, e) { return Object.prototype.hasOwnProperty.call(r, e) }, o.p = "", o(o.s = 0) }([function (r, e, o) { var n = o(1), t = o(4); _rollbarConfig = _rollbarConfig || {}, _rollbarConfig.rollbarJsUrl = _rollbarConfig.rollbarJsUrl || "https://cdnjs.cloudflare.com/ajax/libs/rollbar.js/2.7.1/rollbar.min.js", _rollbarConfig.async = void 0 === _rollbarConfig.async || _rollbarConfig.async; var a = n.setupShim(window, _rollbarConfig), l = t(_rollbarConfig); window.rollbar = n.Rollbar, a.loadFull(window, document, !_rollbarConfig.async, _rollbarConfig, l) }, function (r, e, o) { var n = o(2); function t(r) { return function () { try { return r.apply(this, arguments) } catch (r) { try { console.error("[Rollbar]: Internal error", r) } catch (r) { } } } } var a = 0; function l(r, e) { this.options = r, this._rollbarOldOnError = null; var o = a++; this.shimId = function () { return o }, "undefined" != typeof window && window._rollbarShims && (window._rollbarShims[o] = { handler: e, messages: [] }) } var i = o(3), d = function (r, e) { return new l(r, e) }, c = function (r) { return new i(d, r) }; function s(r) { return t(function () { var e = Array.prototype.slice.call(arguments, 0), o = { shim: this, method: r, args: e, ts: new Date }; window._rollbarShims[this.shimId()].messages.push(o) }) } l.prototype.loadFull = function (r, e, o, n, a) { var l = !1, i = e.createElement("script"), d = e.getElementsByTagName("script")[0], c = d.parentNode; i.crossOrigin = "", i.src = n.rollbarJsUrl, o || (i.async = !0), i.onload = i.onreadystatechange = t(function () { if (!(l || this.readyState && "loaded" !== this.readyState && "complete" !== this.readyState)) { i.onload = i.onreadystatechange = null; try { c.removeChild(i) } catch (r) { } l = !0, function () { var e; if (void 0 === r._rollbarDidLoad) { e = new Error("rollbar.js did not load"); for (var o, n, t, l, i = 0; o = r._rollbarShims[i++];)for (o = o.messages || []; n = o.shift();)for (t = n.args || [], i = 0; i < t.length; ++i)if ("function" == typeof (l = t[i])) { l(e); break } } "function" == typeof a && a(e) }() } }), c.insertBefore(i, d) }, l.prototype.wrap = function (r, e, o) { try { var n; if (n = "function" == typeof e ? e : function () { return e || {} }, "function" != typeof r) return r; if (r._isWrap) return r; if (!r._rollbar_wrapped && (r._rollbar_wrapped = function () { o && "function" == typeof o && o.apply(this, arguments); try { return r.apply(this, arguments) } catch (o) { var e = o; throw e && ("string" == typeof e && (e = new String(e)), e._rollbarContext = n() || {}, e._rollbarContext._wrappedSource = r.toString(), window._rollbarWrappedError = e), e } }, r._rollbar_wrapped._isWrap = !0, r.hasOwnProperty)) for (var t in r) r.hasOwnProperty(t) && (r._rollbar_wrapped[t] = r[t]); return r._rollbar_wrapped } catch (e) { return r } }; for (var p = "log,debug,info,warn,warning,error,critical,global,configure,handleUncaughtException,handleUnhandledRejection,captureEvent,captureDomContentLoaded,captureLoad".split(","), u = 0; u < p.length; ++u)l.prototype[p[u]] = s(p[u]); r.exports = { setupShim: function (r, e) { if (r) { var o = e.globalAlias || "Rollbar"; if ("object" == typeof r[o]) return r[o]; r._rollbarShims = {}, r._rollbarWrappedError = null; var a = new c(e); return t(function () { e.captureUncaught && (a._rollbarOldOnError = r.onerror, n.captureUncaughtExceptions(r, a, !0), n.wrapGlobals(r, a, !0)), e.captureUnhandledRejections && n.captureUnhandledRejections(r, a, !0); var t = e.autoInstrument; return !1 !== e.enabled && (void 0 === t || !0 === t || "object" == typeof t && t.network) && r.addEventListener && (r.addEventListener("load", a.captureLoad.bind(a)), r.addEventListener("DOMContentLoaded", a.captureDomContentLoaded.bind(a))), r[o] = a, a })() } }, Rollbar: c } }, function (r, e) { function o(r, e, o) { if (e.hasOwnProperty && e.hasOwnProperty("addEventListener")) { for (var n = e.addEventListener; n._rollbarOldAdd && n.belongsToShim;)n = n._rLine truncated
|
||||
<div id="root"></div>
|
||||
|
||||
<script>
|
||||
// Note: This is a client access token so there is no issue with having it publicly available
|
||||
var _rollbarConfig = {
|
||||
accessToken: "739c64cd39e84592ba062bc29aac9769",
|
||||
captureUncaught: true,
|
||||
captureUnhandledRejections: true,
|
||||
payload: {
|
||||
environment: "viewer.ohif.org"
|
||||
}
|
||||
};
|
||||
// Rollbar Snippet
|
||||
!function (r) { var e = {}; function o(n) { if (e[n]) return e[n].exports; var t = e[n] = { i: n, l: !1, exports: {} }; return r[n].call(t.exports, t, t.exports, o), t.l = !0, t.exports } o.m = r, o.c = e, o.d = function (r, e, n) { o.o(r, e) || Object.defineProperty(r, e, { enumerable: !0, get: n }) }, o.r = function (r) { "undefined" != typeof Symbol && Symbol.toStringTag && Object.defineProperty(r, Symbol.toStringTag, { value: "Module" }), Object.defineProperty(r, "__esModule", { value: !0 }) }, o.t = function (r, e) { if (1 & e && (r = o(r)), 8 & e) return r; if (4 & e && "object" == typeof r && r && r.__esModule) return r; var n = Object.create(null); if (o.r(n), Object.defineProperty(n, "default", { enumerable: !0, value: r }), 2 & e && "string" != typeof r) for (var t in r) o.d(n, t, function (e) { return r[e] }.bind(null, t)); return n }, o.n = function (r) { var e = r && r.__esModule ? function () { return r.default } : function () { return r }; return o.d(e, "a", e), e }, o.o = function (r, e) { return Object.prototype.hasOwnProperty.call(r, e) }, o.p = "", o(o.s = 0) }([function (r, e, o) { var n = o(1), t = o(4); _rollbarConfig = _rollbarConfig || {}, _rollbarConfig.rollbarJsUrl = _rollbarConfig.rollbarJsUrl || "https://cdnjs.cloudflare.com/ajax/libs/rollbar.js/2.7.1/rollbar.min.js", _rollbarConfig.async = void 0 === _rollbarConfig.async || _rollbarConfig.async; var a = n.setupShim(window, _rollbarConfig), l = t(_rollbarConfig); window.rollbar = n.Rollbar, a.loadFull(window, document, !_rollbarConfig.async, _rollbarConfig, l) }, function (r, e, o) { var n = o(2); function t(r) { return function () { try { return r.apply(this, arguments) } catch (r) { try { console.error("[Rollbar]: Internal error", r) } catch (r) { } } } } var a = 0; function l(r, e) { this.options = r, this._rollbarOldOnError = null; var o = a++; this.shimId = function () { return o }, "undefined" != typeof window && window._rollbarShims && (window._rollbarShims[o] = { handler: e, messages: [] }) } var i = o(3), d = function (r, e) { return new l(r, e) }, c = function (r) { return new i(d, r) }; function s(r) { return t(function () { var e = Array.prototype.slice.call(arguments, 0), o = { shim: this, method: r, args: e, ts: new Date }; window._rollbarShims[this.shimId()].messages.push(o) }) } l.prototype.loadFull = function (r, e, o, n, a) { var l = !1, i = e.createElement("script"), d = e.getElementsByTagName("script")[0], c = d.parentNode; i.crossOrigin = "", i.src = n.rollbarJsUrl, o || (i.async = !0), i.onload = i.onreadystatechange = t(function () { if (!(l || this.readyState && "loaded" !== this.readyState && "complete" !== this.readyState)) { i.onload = i.onreadystatechange = null; try { c.removeChild(i) } catch (r) { } l = !0, function () { var e; if (void 0 === r._rollbarDidLoad) { e = new Error("rollbar.js did not load"); for (var o, n, t, l, i = 0; o = r._rollbarShims[i++];)for (o = o.messages || []; n = o.shift();)for (t = n.args || [], i = 0; i < t.length; ++i)if ("function" == typeof (l = t[i])) { l(e); break } } "function" == typeof a && a(e) }() } }), c.insertBefore(i, d) }, l.prototype.wrap = function (r, e, o) { try { var n; if (n = "function" == typeof e ? e : function () { return e || {} }, "function" != typeof r) return r; if (r._isWrap) return r; if (!r._rollbar_wrapped && (r._rollbar_wrapped = function () { o && "function" == typeof o && o.apply(this, arguments); try { return r.apply(this, arguments) } catch (o) { var e = o; throw e && ("string" == typeof e && (e = new String(e)), e._rollbarContext = n() || {}, e._rollbarContext._wrappedSource = r.toString(), window._rollbarWrappedError = e), e } }, r._rollbar_wrapped._isWrap = !0, r.hasOwnProperty)) for (var t in r) r.hasOwnProperty(t) && (r._rollbar_wrapped[t] = r[t]); return r._rollbar_wrapped } catch (e) { return r } }; for (var p = "log,debug,info,warn,warning,error,critical,global,configure,handleUncaughtException,handleUnhandledRejection,captureEvent,captureDomContentLoaded,captureLoad".split(","), u = 0; u < p.length; ++u)l.prototype[p[u]] = s(p[u]); r.exports = { setupShim: function (r, e) { if (r) { var o = e.globalAlias || "Rollbar"; if ("object" == typeof r[o]) return r[o]; r._rollbarShims = {}, r._rollbarWrappedError = null; var a = new c(e); return t(function () { e.captureUncaught && (a._rollbarOldOnError = r.onerror, n.captureUncaughtExceptions(r, a, !0), n.wrapGlobals(r, a, !0)), e.captureUnhandledRejections && n.captureUnhandledRejections(r, a, !0); var t = e.autoInstrument; return !1 !== e.enabled && (void 0 === t || !0 === t || "object" == typeof t && t.network) && r.addEventListener && (r.addEventListener("load", a.captureLoad.bind(a)), r.addEventListener("DOMContentLoaded", a.captureDomContentLoaded.bind(a))), r[o] = a, a })() } }, Rollbar: c } }, function (r, e) { function o(r, e, o) { if (e.hasOwnProperty && e.hasOwnProperty("addEventListener")) { for (var n = e.addEventListener; n._rollbarOldAdd && n.belongsToShim;)n = n._rolLine truncated
|
||||
// End Rollbar Snippet
|
||||
</script>
|
||||
</body>
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
File diff suppressed because it is too large.
Load diff
+127
-52
@@ -4,9 +4,13 @@ import { I18nextProvider } from 'react-i18next';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Provider } from 'react-redux';
|
||||
import { BrowserRouter as Router } from 'react-router-dom';
|
||||
import OHIFCornerstoneExtension from '@ohif/extension-cornerstone';
|
||||
import { hot } from 'react-hot-loader/root';
|
||||
|
||||
import OHIFCornerstoneExtension from '@ohif/extension-cornerstone';
|
||||
|
||||
import ToolContextMenu from './connectedComponents/ToolContextMenu';
|
||||
import LabellingFlow from './components/Labelling/LabellingFlow';
|
||||
|
||||
import {
|
||||
SnackbarProvider,
|
||||
ModalProvider,
|
||||
@@ -14,16 +18,23 @@ import {
|
||||
OHIFModal,
|
||||
} from '@ohif/ui';
|
||||
|
||||
import {
|
||||
LabellingFlowProvider,
|
||||
ContextMenuProvider,
|
||||
} from './appCustomProviders';
|
||||
|
||||
import {
|
||||
CommandsManager,
|
||||
ExtensionManager,
|
||||
ServicesManager,
|
||||
HotkeysManager,
|
||||
createUINotificationService,
|
||||
createUIModalService,
|
||||
createUIDialogService,
|
||||
UINotificationService,
|
||||
UIModalService,
|
||||
UIDialogService,
|
||||
UIContextMenuService,
|
||||
UILabellingFlowService,
|
||||
utils,
|
||||
redux as reduxOHIF
|
||||
redux as reduxOHIF,
|
||||
} from '@ohif/core';
|
||||
|
||||
import i18n from '@ohif/i18n';
|
||||
@@ -46,12 +57,12 @@ import OHIFStandaloneViewer from './OHIFStandaloneViewer';
|
||||
/** Store */
|
||||
import { getActiveContexts } from './store/layout/selectors.js';
|
||||
import store from './store';
|
||||
const { setUserPreferences } = reduxOHIF.actions;
|
||||
|
||||
/** Contexts */
|
||||
import WhiteLabellingContext from './context/WhiteLabellingContext';
|
||||
import UserManagerContext from './context/UserManagerContext';
|
||||
import AppContext from './context/AppContext';
|
||||
const { setUserPreferences } = reduxOHIF.actions;
|
||||
|
||||
/** ~~~~~~~~~~~~~ Application Setup */
|
||||
const commandsManagerConfig = {
|
||||
@@ -59,11 +70,6 @@ const commandsManagerConfig = {
|
||||
getActiveContexts: () => getActiveContexts(store.getState()),
|
||||
};
|
||||
|
||||
/** Services */
|
||||
const UINotificationService = createUINotificationService();
|
||||
const UIModalService = createUIModalService();
|
||||
const UIDialogService = createUIDialogService();
|
||||
|
||||
/** Managers */
|
||||
const commandsManager = new CommandsManager(commandsManagerConfig);
|
||||
const hotkeysManager = new HotkeysManager(commandsManager);
|
||||
@@ -79,23 +85,25 @@ window.store = store;
|
||||
|
||||
class App extends Component {
|
||||
static propTypes = {
|
||||
routerBasename: PropTypes.string.isRequired,
|
||||
servers: PropTypes.object.isRequired,
|
||||
//
|
||||
oidc: PropTypes.array,
|
||||
whiteLabelling: PropTypes.object,
|
||||
extensions: PropTypes.arrayOf(
|
||||
config: PropTypes.oneOfType([
|
||||
PropTypes.func,
|
||||
PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
})
|
||||
),
|
||||
hotkeys: PropTypes.array,
|
||||
routerBasename: PropTypes.string.isRequired,
|
||||
oidc: PropTypes.array,
|
||||
whiteLabelling: PropTypes.object,
|
||||
extensions: PropTypes.array,
|
||||
}),
|
||||
]).isRequired,
|
||||
defaultExtensions: PropTypes.array,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
whiteLabelling: {},
|
||||
oidc: [],
|
||||
extensions: [],
|
||||
config: {
|
||||
whiteLabelling: {},
|
||||
oidc: [],
|
||||
extensions: [],
|
||||
},
|
||||
defaultExtensions: [],
|
||||
};
|
||||
|
||||
_appConfig;
|
||||
@@ -104,31 +112,67 @@ class App extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this._appConfig = props;
|
||||
const { config, defaultExtensions } = props;
|
||||
|
||||
const { servers, extensions, hotkeys, oidc } = props;
|
||||
const appDefaultConfig = {
|
||||
cornerstoneExtensionConfig: {},
|
||||
extensions: [],
|
||||
routerBasename: '/',
|
||||
whiteLabelling: {},
|
||||
};
|
||||
|
||||
this._appConfig = {
|
||||
...appDefaultConfig,
|
||||
...(typeof config === 'function' ? config({ servicesManager }) : config),
|
||||
};
|
||||
|
||||
const {
|
||||
servers,
|
||||
hotkeys,
|
||||
cornerstoneExtensionConfig,
|
||||
extensions,
|
||||
oidc,
|
||||
} = this._appConfig;
|
||||
|
||||
this.initUserManager(oidc);
|
||||
_initServices([UINotificationService, UIModalService, UIDialogService]);
|
||||
_initExtensions(extensions, hotkeys);
|
||||
_initServices([
|
||||
UINotificationService,
|
||||
UIModalService,
|
||||
UIDialogService,
|
||||
UIContextMenuService,
|
||||
UILabellingFlowService,
|
||||
]);
|
||||
_initExtensions(
|
||||
[...defaultExtensions, ...extensions],
|
||||
cornerstoneExtensionConfig
|
||||
);
|
||||
|
||||
/*
|
||||
* Must run after extension commands are registered
|
||||
* if there is no hotkeys from localStorage set up from config.
|
||||
*/
|
||||
_initHotkeys(hotkeys);
|
||||
_initServers(servers);
|
||||
initWebWorkers();
|
||||
}
|
||||
|
||||
render() {
|
||||
const { whiteLabelling, routerBasename } = this.props;
|
||||
const userManager = this._userManager;
|
||||
const config = {
|
||||
appConfig: this._appConfig,
|
||||
};
|
||||
const { whiteLabelling, routerBasename } = this._appConfig;
|
||||
const {
|
||||
UINotificationService,
|
||||
UIDialogService,
|
||||
UILabellingFlowService,
|
||||
UIModalService,
|
||||
UIContextMenuService,
|
||||
} = servicesManager.services;
|
||||
|
||||
if (userManager) {
|
||||
if (this._userManager) {
|
||||
return (
|
||||
<AppContext.Provider value={config}>
|
||||
<AppContext.Provider value={{ appConfig: this._appConfig }}>
|
||||
<Provider store={store}>
|
||||
<I18nextProvider i18n={i18n}>
|
||||
<OidcProvider store={store} userManager={userManager}>
|
||||
<UserManagerContext.Provider value={userManager}>
|
||||
<OidcProvider store={store} userManager={this._userManager}>
|
||||
<UserManagerContext.Provider value={this._userManager}>
|
||||
<Router basename={routerBasename}>
|
||||
<WhiteLabellingContext.Provider value={whiteLabelling}>
|
||||
<SnackbarProvider service={UINotificationService}>
|
||||
@@ -137,7 +181,21 @@ class App extends Component {
|
||||
modal={OHIFModal}
|
||||
service={UIModalService}
|
||||
>
|
||||
<OHIFStandaloneViewer userManager={userManager} />
|
||||
<LabellingFlowProvider
|
||||
service={UILabellingFlowService}
|
||||
labellingComponent={LabellingFlow}
|
||||
commandsManager={commandsManager}
|
||||
>
|
||||
<ContextMenuProvider
|
||||
service={UIContextMenuService}
|
||||
contextMenuComponent={ToolContextMenu}
|
||||
commandsManager={commandsManager}
|
||||
>
|
||||
<OHIFStandaloneViewer
|
||||
userManager={this._userManager}
|
||||
/>
|
||||
</ContextMenuProvider>
|
||||
</LabellingFlowProvider>
|
||||
</ModalProvider>
|
||||
</DialogProvider>
|
||||
</SnackbarProvider>
|
||||
@@ -152,7 +210,7 @@ class App extends Component {
|
||||
}
|
||||
|
||||
return (
|
||||
<AppContext.Provider value={config}>
|
||||
<AppContext.Provider value={{ appConfig: this._appConfig }}>
|
||||
<Provider store={store}>
|
||||
<I18nextProvider i18n={i18n}>
|
||||
<Router basename={routerBasename}>
|
||||
@@ -160,7 +218,19 @@ class App extends Component {
|
||||
<SnackbarProvider service={UINotificationService}>
|
||||
<DialogProvider service={UIDialogService}>
|
||||
<ModalProvider modal={OHIFModal} service={UIModalService}>
|
||||
<OHIFStandaloneViewer />
|
||||
<LabellingFlowProvider
|
||||
service={UILabellingFlowService}
|
||||
labellingComponent={LabellingFlow}
|
||||
commandsManager={commandsManager}
|
||||
>
|
||||
<ContextMenuProvider
|
||||
service={UIContextMenuService}
|
||||
contextMenuComponent={ToolContextMenu}
|
||||
commandsManager={commandsManager}
|
||||
>
|
||||
<OHIFStandaloneViewer />
|
||||
</ContextMenuProvider>
|
||||
</LabellingFlowProvider>
|
||||
</ModalProvider>
|
||||
</DialogProvider>
|
||||
</SnackbarProvider>
|
||||
@@ -174,10 +244,10 @@ class App extends Component {
|
||||
|
||||
initUserManager(oidc) {
|
||||
if (oidc && !!oidc.length) {
|
||||
const firstOpenIdClient = this.props.oidc[0];
|
||||
const firstOpenIdClient = this._appConfig.oidc[0];
|
||||
|
||||
const { protocol, host } = window.location;
|
||||
const { routerBasename } = this.props;
|
||||
const { routerBasename } = this._appConfig;
|
||||
const baseUri = `${protocol}//${host}${routerBasename}`;
|
||||
|
||||
const redirect_uri = firstOpenIdClient.redirect_uri || '/callback';
|
||||
@@ -213,22 +283,22 @@ function _initServices(services) {
|
||||
/**
|
||||
* @param
|
||||
*/
|
||||
function _initExtensions(extensions, hotkeys) {
|
||||
const defaultExtensions = [
|
||||
function _initExtensions(extensions, cornerstoneExtensionConfig) {
|
||||
const requiredExtensions = [
|
||||
GenericViewerCommands,
|
||||
OHIFCornerstoneExtension,
|
||||
// WARNING: MUST BE REGISTERED _AFTER_ OHIFCORNERSTONEEXTENSION
|
||||
[OHIFCornerstoneExtension, cornerstoneExtensionConfig],
|
||||
/* WARNING: MUST BE REGISTERED _AFTER_ OHIFCornerstoneExtension */
|
||||
MeasurementsPanel,
|
||||
];
|
||||
const mergedExtensions = defaultExtensions.concat(extensions);
|
||||
const mergedExtensions = requiredExtensions.concat(extensions);
|
||||
extensionManager.registerExtensions(mergedExtensions);
|
||||
}
|
||||
|
||||
function _initHotkeys(hotkeys) {
|
||||
const { hotkeyDefinitions = {} } = store.getState().preferences || {};
|
||||
let updateStore = false;
|
||||
let hotkeysToUse = hotkeyDefinitions;
|
||||
|
||||
// Must run after extension commands are registered
|
||||
// if there is no hotkeys from localStorate set up from config
|
||||
if (!Object.keys(hotkeyDefinitions).length) {
|
||||
hotkeysToUse = hotkeys;
|
||||
updateStore = true;
|
||||
@@ -236,7 +306,8 @@ function _initExtensions(extensions, hotkeys) {
|
||||
|
||||
if (hotkeysToUse) {
|
||||
hotkeysManager.setHotkeys(hotkeysToUse);
|
||||
// set default based on app config
|
||||
|
||||
/* Set hotkeys default based on app config. */
|
||||
hotkeysManager.setDefaultHotKeys(hotkeys);
|
||||
|
||||
if (updateStore) {
|
||||
@@ -264,7 +335,9 @@ function _makeAbsoluteIfNecessary(url, base_url) {
|
||||
return url;
|
||||
}
|
||||
|
||||
// Make sure base_url and url are not duplicating slashes
|
||||
/*
|
||||
* Make sure base_url and url are not duplicating slashes.
|
||||
*/
|
||||
if (base_url[base_url.length - 1] === '/') {
|
||||
base_url = base_url.slice(0, base_url.length - 1);
|
||||
}
|
||||
@@ -272,7 +345,9 @@ function _makeAbsoluteIfNecessary(url, base_url) {
|
||||
return base_url + url;
|
||||
}
|
||||
|
||||
// Only wrap/use hot if in dev
|
||||
/*
|
||||
* Only wrap/use hot if in dev.
|
||||
*/
|
||||
const ExportedApp = process.env.NODE_ENV === 'development' ? hot(App) : App;
|
||||
|
||||
export default ExportedApp;
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { ContextMenuProvider } from '@ohif/ui';
|
||||
|
||||
const CustomContextMenuProvider = ({
|
||||
children,
|
||||
service,
|
||||
contextMenuComponent,
|
||||
commandsManager,
|
||||
}) => {
|
||||
const onDeleteHandler = (nearbyToolData, eventData) => {
|
||||
const element = eventData.element;
|
||||
commandsManager.runCommand('removeToolState', {
|
||||
element,
|
||||
toolType: nearbyToolData.toolType,
|
||||
tool: nearbyToolData.tool,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<ContextMenuProvider
|
||||
service={service}
|
||||
contextMenuComponent={contextMenuComponent}
|
||||
onDelete={onDeleteHandler}
|
||||
>
|
||||
{children}
|
||||
</ContextMenuProvider>
|
||||
);
|
||||
};
|
||||
|
||||
CustomContextMenuProvider.defaultProps = {
|
||||
service: null,
|
||||
};
|
||||
|
||||
CustomContextMenuProvider.propTypes = {
|
||||
children: PropTypes.oneOfType([
|
||||
PropTypes.arrayOf(PropTypes.node),
|
||||
PropTypes.node,
|
||||
]).isRequired,
|
||||
service: PropTypes.shape({
|
||||
setServiceImplementation: PropTypes.func,
|
||||
}),
|
||||
contextMenuComponent: PropTypes.oneOfType([
|
||||
PropTypes.arrayOf(PropTypes.node),
|
||||
PropTypes.node,
|
||||
PropTypes.func,
|
||||
]).isRequired,
|
||||
commandsManager: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export default CustomContextMenuProvider;
|
||||
@@ -0,0 +1,49 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { LabellingFlowProvider } from '@ohif/ui';
|
||||
|
||||
const CustomLabellingFlowProvider = ({
|
||||
children,
|
||||
service,
|
||||
labellingComponent,
|
||||
commandsManager,
|
||||
}) => {
|
||||
const onUpdateLabellingHandler = (labellingData, measurementData) => {
|
||||
commandsManager.runCommand(
|
||||
'updateTableWithNewMeasurementData',
|
||||
measurementData
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<LabellingFlowProvider
|
||||
service={service}
|
||||
labellingComponent={labellingComponent}
|
||||
onUpdateLabelling={onUpdateLabellingHandler}
|
||||
>
|
||||
{children}
|
||||
</LabellingFlowProvider>
|
||||
);
|
||||
};
|
||||
|
||||
CustomLabellingFlowProvider.defaultProps = {
|
||||
service: null,
|
||||
};
|
||||
|
||||
CustomLabellingFlowProvider.propTypes = {
|
||||
children: PropTypes.oneOfType([
|
||||
PropTypes.arrayOf(PropTypes.node),
|
||||
PropTypes.node,
|
||||
]).isRequired,
|
||||
service: PropTypes.shape({
|
||||
setServiceImplementation: PropTypes.func,
|
||||
}),
|
||||
labellingComponent: PropTypes.oneOfType([
|
||||
PropTypes.arrayOf(PropTypes.node),
|
||||
PropTypes.node,
|
||||
PropTypes.func,
|
||||
]).isRequired,
|
||||
commandsManager: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export default CustomLabellingFlowProvider;
|
||||
@@ -0,0 +1,6 @@
|
||||
export {
|
||||
default as LabellingFlowProvider,
|
||||
} from './LabellingFlowProvider/LabellingFlowProvider.js';
|
||||
export {
|
||||
default as ContextMenuProvider,
|
||||
} from './ContextMenuProvider/ContextMenuProvider.js';
|
||||
@@ -3,14 +3,20 @@ import { MeasurementTable } from '@ohif/ui';
|
||||
import OHIF from '@ohif/core';
|
||||
import moment from 'moment';
|
||||
import cornerstone from 'cornerstone-core';
|
||||
import DICOMSR from './../../lib/DICOMSR';
|
||||
|
||||
//
|
||||
import jumpToRowItem from './jumpToRowItem.js';
|
||||
import getMeasurementLocationCallback from './getMeasurementLocationCallback';
|
||||
|
||||
const { setViewportSpecificData } = OHIF.redux.actions;
|
||||
const { MeasurementApi } = OHIF.measurements;
|
||||
|
||||
/**
|
||||
* Takes a list of objects and a property and return the list grouped by the property
|
||||
*
|
||||
* @param {Array} list - The objects to be grouped by
|
||||
* @param {string} props - The property to group the objects
|
||||
* @returns {Object}
|
||||
*/
|
||||
function groupBy(list, props) {
|
||||
return list.reduce((a, b) => {
|
||||
(a[b[props]] = a[b[props]] || []).push(b);
|
||||
@@ -18,16 +24,29 @@ function groupBy(list, props) {
|
||||
}, {});
|
||||
}
|
||||
|
||||
function getAllTools() {
|
||||
const config = OHIF.measurements.MeasurementApi.getConfiguration();
|
||||
/**
|
||||
* Takes a list of tools grouped and return all tools separately
|
||||
*
|
||||
* @param {Array} [toolGroups=[]] - The grouped tools
|
||||
* @returns {Array} - The list of all tools on all groups
|
||||
*/
|
||||
function getAllTools(toolGroups = []) {
|
||||
let tools = [];
|
||||
config.measurementTools.forEach(
|
||||
toolGroups.forEach(
|
||||
toolGroup => (tools = tools.concat(toolGroup.childTools))
|
||||
);
|
||||
|
||||
return tools;
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes measurementData and build the measurement text to be used into the table
|
||||
*
|
||||
* @param {Object} [measurementData={}]
|
||||
* @param {string} measurementData.location - The measurement location
|
||||
* @param {string} measurementData.description - The measurement description
|
||||
* @returns {string}
|
||||
*/
|
||||
function getMeasurementText(measurementData = {}) {
|
||||
const defaultText = '...';
|
||||
const { location = '', description = '' } = measurementData;
|
||||
@@ -36,6 +55,14 @@ function getMeasurementText(measurementData = {}) {
|
||||
return result || defaultText;
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a list of measurements grouped by measurement numbers and return each measurement data by available timepoint
|
||||
*
|
||||
* @param {Array} measurementNumberList - The list of measurements
|
||||
* @param {Array} timepoints - The list of available timepoints
|
||||
* @param {Function} displayFunction - The function that builds the display text by each tool
|
||||
* @returns
|
||||
*/
|
||||
function getDataForEachMeasurementNumber(
|
||||
measurementNumberList,
|
||||
timepoints,
|
||||
@@ -58,10 +85,35 @@ function getDataForEachMeasurementNumber(
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Take a measurement toolName and return if any warnings
|
||||
*
|
||||
* @param {string} toolName - The tool name
|
||||
* @returns {string}
|
||||
*/
|
||||
function getWarningsForMeasurement(toolName) {
|
||||
const isToolSupported = DICOMSR.isToolSupported(toolName);
|
||||
|
||||
return {
|
||||
hasWarnings: !isToolSupported,
|
||||
warningTitle: isToolSupported ? '' : 'Unsupported Tool',
|
||||
warningList: isToolSupported
|
||||
? []
|
||||
: [`${toolName} cannot be persisted at this time`],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Take measurements from MeasurementAPI structure and convert into a measurementTable structure
|
||||
*
|
||||
* @param {Object} toolCollections - The list of all measurement grouped by groupTool and toolName
|
||||
* @param {Array} timepoints - The list of available timepoints
|
||||
* @returns
|
||||
*/
|
||||
function convertMeasurementsToTableData(toolCollections, timepoints) {
|
||||
const config = OHIF.measurements.MeasurementApi.getConfiguration();
|
||||
const toolGroups = config.measurementTools;
|
||||
const tools = getAllTools();
|
||||
const tools = getAllTools(toolGroups);
|
||||
|
||||
const tableMeasurements = toolGroups.map(toolGroup => {
|
||||
return {
|
||||
@@ -90,6 +142,12 @@ function convertMeasurementsToTableData(toolCollections, timepoints) {
|
||||
} = measurementData;
|
||||
const measurementId = measurementData._id;
|
||||
|
||||
const {
|
||||
hasWarnings,
|
||||
warningTitle,
|
||||
warningList,
|
||||
} = getWarningsForMeasurement(toolType);
|
||||
|
||||
//check if all measurements with same measurementNumber will have same LABEL
|
||||
const tableMeasurement = {
|
||||
itemNumber: lesionNamingNumber,
|
||||
@@ -98,10 +156,10 @@ function convertMeasurementsToTableData(toolCollections, timepoints) {
|
||||
measurementNumber,
|
||||
lesionNamingNumber,
|
||||
toolType,
|
||||
hasWarnings: false, //TODO
|
||||
warningTitle: '', //TODO
|
||||
hasWarnings,
|
||||
warningTitle,
|
||||
warningList,
|
||||
isSplitLesion: false, //TODO
|
||||
warningList: [], //TODO
|
||||
data: getDataForEachMeasurementNumber(
|
||||
measurementNumberList,
|
||||
timepoints,
|
||||
@@ -128,6 +186,13 @@ function convertMeasurementsToTableData(toolCollections, timepoints) {
|
||||
return tableMeasurements;
|
||||
}
|
||||
|
||||
/**
|
||||
* Take a list of available timepoints and return a list header information for each timepoint
|
||||
*
|
||||
* @param {Array} timepoints - The list of available timepoints
|
||||
* @param {string} timepoints[].latestDate - The date of the last study taken on the timepoint
|
||||
* @returns {{label: string, key: string, date: string}[]}
|
||||
*/
|
||||
function convertTimepointsToTableData(timepoints) {
|
||||
if (!timepoints || !timepoints.length) {
|
||||
return [];
|
||||
@@ -142,8 +207,28 @@ function convertTimepointsToTableData(timepoints) {
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes server type and return a function or undefined
|
||||
*
|
||||
* @param {string} serverType - The server type
|
||||
* @returns {undefined|Function}
|
||||
*/
|
||||
function getSaveFunction(serverType) {
|
||||
if (serverType === 'dicomWeb') {
|
||||
return () => {
|
||||
const measurementApi = OHIF.measurements.MeasurementApi.Instance;
|
||||
const promise = measurementApi.storeMeasurements();
|
||||
return promise;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const { timepoints, measurements } = state.timepointManager;
|
||||
const { timepointManager, servers } = state;
|
||||
const { timepoints, measurements } = timepointManager;
|
||||
const activeServer = servers.servers.find(a => a.active === true);
|
||||
const saveFunction = getSaveFunction(activeServer.type);
|
||||
|
||||
return {
|
||||
timepoints: convertTimepointsToTableData(timepoints),
|
||||
measurementCollection: convertMeasurementsToTableData(
|
||||
@@ -152,12 +237,15 @@ const mapStateToProps = state => {
|
||||
),
|
||||
timepointManager: state.timepointManager,
|
||||
viewports: state.viewports,
|
||||
saveFunction,
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
const mapDispatchToProps = (dispatch, ownProps) => {
|
||||
return {
|
||||
dispatchRelabel: (event, measurementData, viewportsState) => {
|
||||
event.persist();
|
||||
|
||||
const activeViewportIndex =
|
||||
(viewportsState && viewportsState.activeViewportIndex) || 0;
|
||||
|
||||
@@ -167,31 +255,21 @@ const mapDispatchToProps = dispatch => {
|
||||
return;
|
||||
}
|
||||
|
||||
const { element } = enabledElements[activeViewportIndex];
|
||||
|
||||
const eventData = {
|
||||
event: {
|
||||
clientX: event.clientX,
|
||||
clientY: event.clientY,
|
||||
},
|
||||
element,
|
||||
};
|
||||
|
||||
const { toolType, measurementId } = measurementData;
|
||||
const tool = MeasurementApi.Instance.tools[toolType].find(measurement => {
|
||||
return measurement._id === measurementId;
|
||||
});
|
||||
|
||||
const options = {
|
||||
skipAddLabelButton: true,
|
||||
editLocation: true,
|
||||
};
|
||||
|
||||
// Clone the tool not to set empty location initially
|
||||
const toolForLocation = Object.assign({}, tool, { location: null });
|
||||
getMeasurementLocationCallback(eventData, toolForLocation, options);
|
||||
|
||||
if (ownProps.onRelabel) {
|
||||
ownProps.onRelabel(toolForLocation);
|
||||
}
|
||||
},
|
||||
dispatchEditDescription: (event, measurementData, viewportsState) => {
|
||||
event.persist();
|
||||
|
||||
const activeViewportIndex =
|
||||
(viewportsState && viewportsState.activeViewportIndex) || 0;
|
||||
|
||||
@@ -201,26 +279,14 @@ const mapDispatchToProps = dispatch => {
|
||||
return;
|
||||
}
|
||||
|
||||
const { element } = enabledElements[activeViewportIndex];
|
||||
|
||||
const eventData = {
|
||||
event: {
|
||||
clientX: event.clientX,
|
||||
clientY: event.clientY,
|
||||
},
|
||||
element,
|
||||
};
|
||||
|
||||
const { toolType, measurementId } = measurementData;
|
||||
const tool = MeasurementApi.Instance.tools[toolType].find(measurement => {
|
||||
return measurement._id === measurementId;
|
||||
});
|
||||
|
||||
const options = {
|
||||
editDescriptionOnDialog: true,
|
||||
};
|
||||
|
||||
getMeasurementLocationCallback(eventData, tool, options);
|
||||
if (ownProps.onEditDescription) {
|
||||
ownProps.onEditDescription(tool);
|
||||
}
|
||||
},
|
||||
dispatchJumpToRowItem: (
|
||||
measurementData,
|
||||
@@ -287,10 +353,15 @@ const mapDispatchToProps = dispatch => {
|
||||
};
|
||||
|
||||
const mergeProps = (propsFromState, propsFromDispatch, ownProps) => {
|
||||
const { timepoints, saveFunction, measurementCollection } = propsFromState;
|
||||
const { onSaveComplete, selectedMeasurementNumber } = ownProps;
|
||||
|
||||
return {
|
||||
timepoints: propsFromState.timepoints,
|
||||
measurementCollection: propsFromState.measurementCollection,
|
||||
selectedMeasurementNumber: ownProps.selectedMeasurementNumber,
|
||||
timepoints,
|
||||
saveFunction,
|
||||
measurementCollection,
|
||||
onSaveComplete,
|
||||
selectedMeasurementNumber,
|
||||
...propsFromDispatch,
|
||||
onItemClick: (event, measurementData) => {
|
||||
// TODO: Add timepointId to .data for measurementData?
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
const setLabellingFlowDataAction = labellingFlowData => ({
|
||||
type: 'SET_LABELLING_FLOW_DATA',
|
||||
labellingFlowData,
|
||||
});
|
||||
|
||||
const resetLabellingAndContextMenuAction = state => ({
|
||||
type: 'RESET_LABELLING_AND_CONTEXT_MENU',
|
||||
state,
|
||||
});
|
||||
|
||||
const setToolContextMenuDataAction = (viewportIndex, toolContextMenuData) => ({
|
||||
type: 'SET_TOOL_CONTEXT_MENU_DATA',
|
||||
viewportIndex,
|
||||
toolContextMenuData,
|
||||
});
|
||||
|
||||
export {
|
||||
resetLabellingAndContextMenuAction,
|
||||
setLabellingFlowDataAction,
|
||||
setToolContextMenuDataAction,
|
||||
};
|
||||
@@ -1,33 +0,0 @@
|
||||
import cornerstoneTools from 'cornerstone-tools';
|
||||
import updateTableWithNewMeasurementData from './updateTableWithNewMeasurementData.js';
|
||||
|
||||
export default function getMeasurementLocationCallback(
|
||||
eventData,
|
||||
tool,
|
||||
options
|
||||
) {
|
||||
const { toolType } = tool;
|
||||
const { element } = eventData;
|
||||
const doneCallback = updateTableWithNewMeasurementData;
|
||||
|
||||
const ToolInstance = cornerstoneTools.getToolForElement(element, toolType);
|
||||
|
||||
if (
|
||||
!ToolInstance ||
|
||||
!ToolInstance.configuration ||
|
||||
!ToolInstance.configuration.getMeasurementLocationCallback
|
||||
) {
|
||||
console.warn(
|
||||
'Tool instance configuration is missing: getMeasurementLocationCallback'
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
ToolInstance.configuration.getMeasurementLocationCallback(
|
||||
tool,
|
||||
eventData,
|
||||
doneCallback,
|
||||
options
|
||||
);
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import React from 'react';
|
||||
import ConnectedMeasurementTable from './ConnectedMeasurementTable.js';
|
||||
import init from './init.js';
|
||||
|
||||
@@ -7,10 +8,44 @@ export default {
|
||||
*/
|
||||
id: 'measurements-table',
|
||||
|
||||
preRegistration({ servicesManager, configuration = {} }) {
|
||||
init({ servicesManager, configuration });
|
||||
preRegistration({ servicesManager, commandsManager, configuration = {} }) {
|
||||
init({ servicesManager, commandsManager, configuration });
|
||||
},
|
||||
getPanelModule({ servicesManager }) {
|
||||
|
||||
getPanelModule({ servicesManager, commandsManager }) {
|
||||
const { UILabellingFlowService, UINotificationService } = servicesManager.services;
|
||||
const ExtendedConnectedMeasurementTable = () => (
|
||||
<ConnectedMeasurementTable
|
||||
onRelabel={tool => {
|
||||
if (UILabellingFlowService) {
|
||||
UILabellingFlowService.show({
|
||||
centralize: true,
|
||||
props: {
|
||||
skipAddLabelButton: true,
|
||||
editLocation: true,
|
||||
measurementData: tool,
|
||||
},
|
||||
});
|
||||
}
|
||||
}}
|
||||
onEditDescription={tool => {
|
||||
if (UILabellingFlowService) {
|
||||
UILabellingFlowService.show({
|
||||
centralize: true,
|
||||
props: {
|
||||
editDescriptionOnDialog: true,
|
||||
measurementData: tool,
|
||||
},
|
||||
});
|
||||
}
|
||||
}}
|
||||
onSaveComplete={message => {
|
||||
if (UINotificationService) {
|
||||
UINotificationService.show(message);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
return {
|
||||
menuOptions: [
|
||||
{
|
||||
@@ -22,7 +57,7 @@ export default {
|
||||
components: [
|
||||
{
|
||||
id: 'measurement-panel',
|
||||
component: ConnectedMeasurementTable,
|
||||
component: ExtendedConnectedMeasurementTable,
|
||||
},
|
||||
],
|
||||
defaultContext: ['VIEWER'],
|
||||
|
||||
@@ -1,19 +1,7 @@
|
||||
import OHIF from '@ohif/core';
|
||||
import cornerstone from 'cornerstone-core';
|
||||
import csTools from 'cornerstone-tools';
|
||||
import {
|
||||
getToolLabellingFlowCallback,
|
||||
getOnRightClickCallback,
|
||||
getOnTouchPressCallback,
|
||||
getResetLabellingAndContextMenu,
|
||||
} from './labelingFlowCallbacks.js';
|
||||
import throttle from 'lodash.throttle';
|
||||
import { SimpleDialog } from '@ohif/ui';
|
||||
|
||||
// TODO: This only works because we have a hard dependency on this extension
|
||||
// We need to decouple and make stuff like this possible w/o bundling this at
|
||||
// build time
|
||||
import store from './../../store';
|
||||
|
||||
const {
|
||||
onAdded,
|
||||
@@ -33,92 +21,18 @@ const MEASUREMENT_ACTION_MAP = {
|
||||
*
|
||||
*
|
||||
* @export
|
||||
* @param {*} configuration
|
||||
* @param {Object} servicesManager
|
||||
* @param {Object} configuration
|
||||
*/
|
||||
export default function init({ servicesManager, configuration = {} }) {
|
||||
const { UIDialogService } = servicesManager.services;
|
||||
const callInputDialog = (data, event, callback) => {
|
||||
let dialogId = UIDialogService.create({
|
||||
content: SimpleDialog.InputDialog,
|
||||
defaultPosition: {
|
||||
x: (event && event.currentPoints.canvas.x) || 0,
|
||||
y: (event && event.currentPoints.canvas.y) || 0,
|
||||
},
|
||||
showOverlay: true,
|
||||
contentProps: {
|
||||
title: 'Enter your annotation',
|
||||
label: 'New label',
|
||||
defaultValue: data ? data.text : '',
|
||||
onClose: () => UIDialogService.dismiss({ id: dialogId }),
|
||||
onSubmit: value => {
|
||||
callback(value);
|
||||
UIDialogService.dismiss({ id: dialogId });
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// If these tools were already added by a different extension, we want to replace
|
||||
// them with the same tools that have an alternative configuration. By passing in
|
||||
// our custom `getMeasurementLocationCallback`, we can...
|
||||
const toolLabellingFlowCallback = getToolLabellingFlowCallback(store);
|
||||
|
||||
// Removes all tools from all enabled elements w/ provided name
|
||||
// Not commonly used API, so :eyes: for unknown side-effects
|
||||
csTools.removeTool('Bidirectional');
|
||||
csTools.removeTool('Length');
|
||||
csTools.removeTool('Angle');
|
||||
csTools.removeTool('FreehandRoi');
|
||||
csTools.removeTool('EllipticalRoi');
|
||||
csTools.removeTool('CircleRoi');
|
||||
csTools.removeTool('RectangleRoi');
|
||||
csTools.removeTool('ArrowAnnotate');
|
||||
|
||||
// Re-add each tool w/ our custom configuration
|
||||
csTools.addTool(csTools.BidirectionalTool, {
|
||||
configuration: {
|
||||
getMeasurementLocationCallback: toolLabellingFlowCallback,
|
||||
},
|
||||
});
|
||||
csTools.addTool(csTools.LengthTool, {
|
||||
configuration: {
|
||||
getMeasurementLocationCallback: toolLabellingFlowCallback,
|
||||
},
|
||||
});
|
||||
csTools.addTool(csTools.AngleTool, {
|
||||
configuration: {
|
||||
getMeasurementLocationCallback: toolLabellingFlowCallback,
|
||||
},
|
||||
});
|
||||
csTools.addTool(csTools.FreehandRoiTool, {
|
||||
configuration: {
|
||||
getMeasurementLocationCallback: toolLabellingFlowCallback,
|
||||
},
|
||||
});
|
||||
csTools.addTool(csTools.EllipticalRoiTool, {
|
||||
configuration: {
|
||||
getMeasurementLocationCallback: toolLabellingFlowCallback,
|
||||
},
|
||||
});
|
||||
csTools.addTool(csTools.CircleRoiTool, {
|
||||
configuration: {
|
||||
getMeasurementLocationCallback: toolLabellingFlowCallback,
|
||||
},
|
||||
});
|
||||
csTools.addTool(csTools.RectangleRoiTool, {
|
||||
configuration: {
|
||||
getMeasurementLocationCallback: toolLabellingFlowCallback,
|
||||
},
|
||||
});
|
||||
csTools.addTool(csTools.ArrowAnnotateTool, {
|
||||
configuration: {
|
||||
getMeasurementLocationCallback: toolLabellingFlowCallback,
|
||||
getTextCallback: (callback, eventDetails) =>
|
||||
callInputDialog(null, eventDetails, callback),
|
||||
changeTextCallback: (data, eventDetails, callback) =>
|
||||
callInputDialog(data, eventDetails, callback),
|
||||
},
|
||||
});
|
||||
export default function init({
|
||||
servicesManager,
|
||||
commandsManager,
|
||||
configuration,
|
||||
}) {
|
||||
const {
|
||||
UIContextMenuService,
|
||||
UILabellingFlowService,
|
||||
} = servicesManager.services;
|
||||
|
||||
// TODO: MEASUREMENT_COMPLETED (not present in initial implementation)
|
||||
const onMeasurementsChanged = (action, event) => {
|
||||
@@ -131,15 +45,43 @@ export default function init({ servicesManager, configuration = {} }) {
|
||||
this,
|
||||
'labelmapModified'
|
||||
);
|
||||
//
|
||||
const onRightClick = getOnRightClickCallback(store);
|
||||
const onTouchPress = getOnTouchPressCallback(store);
|
||||
const onNewImage = getResetLabellingAndContextMenu(store);
|
||||
const onMouseClick = getResetLabellingAndContextMenu(store);
|
||||
const onTouchStart = getResetLabellingAndContextMenu(store);
|
||||
|
||||
// Because click gives us the native "mouse up", buttons will always be `0`
|
||||
// Need to fallback to event.which;
|
||||
const onRightClick = event => {
|
||||
if (UIContextMenuService) {
|
||||
UIContextMenuService.show({ event: event.detail });
|
||||
}
|
||||
};
|
||||
|
||||
const onTouchPress = event => {
|
||||
if (UIContextMenuService) {
|
||||
UIContextMenuService.show({
|
||||
event: event.detail,
|
||||
props: {
|
||||
isTouchEvent: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const onTouchStart = () => resetLabelligAndContextMenu();
|
||||
|
||||
const onMouseClick = () => resetLabelligAndContextMenu();
|
||||
|
||||
const resetLabelligAndContextMenu = () => {
|
||||
if (UILabellingFlowService && UIContextMenuService) {
|
||||
UILabellingFlowService.hide();
|
||||
UIContextMenuService.hide();
|
||||
}
|
||||
};
|
||||
|
||||
// TODO: This makes scrolling painfully slow
|
||||
// const onNewImage = ...
|
||||
|
||||
/*
|
||||
* Because click gives us the native "mouse up", buttons will always be `0`
|
||||
* Need to fallback to event.which;
|
||||
*
|
||||
*/
|
||||
const handleClick = cornerstoneMouseClickEvent => {
|
||||
const mouseUpEvent = cornerstoneMouseClickEvent.detail.event;
|
||||
const isRightClick = mouseUpEvent.which === 3;
|
||||
@@ -170,10 +112,11 @@ export default function init({ servicesManager, configuration = {} }) {
|
||||
csTools.EVENTS.LABELMAP_MODIFIED,
|
||||
onLabelmapModified
|
||||
);
|
||||
//
|
||||
|
||||
element.addEventListener(csTools.EVENTS.TOUCH_PRESS, onTouchPress);
|
||||
element.addEventListener(csTools.EVENTS.MOUSE_CLICK, handleClick);
|
||||
element.addEventListener(csTools.EVENTS.TOUCH_START, onTouchStart);
|
||||
|
||||
// TODO: This makes scrolling painfully slow
|
||||
// element.addEventListener(cornerstone.EVENTS.NEW_IMAGE, onNewImage);
|
||||
}
|
||||
@@ -197,10 +140,12 @@ export default function init({ servicesManager, configuration = {} }) {
|
||||
csTools.EVENTS.LABELMAP_MODIFIED,
|
||||
onLabelmapModified
|
||||
);
|
||||
//
|
||||
|
||||
element.removeEventListener(csTools.EVENTS.TOUCH_PRESS, onTouchPress);
|
||||
element.removeEventListener(csTools.EVENTS.MOUSE_CLICK, handleClick);
|
||||
element.removeEventListener(csTools.EVENTS.TOUCH_START, onTouchStart);
|
||||
|
||||
// TODO: This makes scrolling painfully slow
|
||||
// element.removeEventListener(cornerstone.EVENTS.NEW_IMAGE, onNewImage);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,144 +0,0 @@
|
||||
import {
|
||||
resetLabellingAndContextMenuAction,
|
||||
setToolContextMenuDataAction,
|
||||
setLabellingFlowDataAction,
|
||||
} from './actions.js';
|
||||
import updateTableWithNewMeasurementData from './updateTableWithNewMeasurementData.js';
|
||||
|
||||
const VIEWPORT_INDEX = 0;
|
||||
|
||||
function getOnRightClickCallback(store) {
|
||||
const setToolContextMenuData = (viewportIndex, toolContextMenuData) => {
|
||||
store.dispatch(resetLabellingAndContextMenuAction());
|
||||
store.dispatch(
|
||||
setToolContextMenuDataAction(viewportIndex, toolContextMenuData)
|
||||
);
|
||||
};
|
||||
|
||||
const getOnCloseCallback = viewportIndex => {
|
||||
return function onClose() {
|
||||
const toolContextMenuData = {
|
||||
visible: false,
|
||||
};
|
||||
|
||||
store.dispatch(
|
||||
setToolContextMenuDataAction(viewportIndex, toolContextMenuData)
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
return function onRightClick(event) {
|
||||
const eventData = event.detail;
|
||||
const viewportIndex = VIEWPORT_INDEX; // parseInt(eventData.element.dataset.viewportIndex, 10);
|
||||
|
||||
const toolContextMenuData = {
|
||||
eventData,
|
||||
isTouchEvent: false,
|
||||
onClose: getOnCloseCallback(viewportIndex),
|
||||
};
|
||||
|
||||
// setToolContextMenuData(viewportIndex, toolContextMenuData);
|
||||
setToolContextMenuData(0, toolContextMenuData);
|
||||
};
|
||||
}
|
||||
|
||||
function getOnTouchPressCallback(store) {
|
||||
const setToolContextMenuData = (viewportIndex, toolContextMenuData) => {
|
||||
store.dispatch(resetLabellingAndContextMenuAction());
|
||||
store.dispatch(
|
||||
setToolContextMenuDataAction(viewportIndex, toolContextMenuData)
|
||||
);
|
||||
};
|
||||
|
||||
const getOnCloseCallback = viewportIndex => {
|
||||
return function onClose() {
|
||||
const toolContextMenuData = {
|
||||
visible: false,
|
||||
};
|
||||
|
||||
store.dispatch(
|
||||
setToolContextMenuDataAction(viewportIndex, toolContextMenuData)
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
return function onTouchPress(event) {
|
||||
const eventData = event.detail;
|
||||
const viewportIndex = parseInt(eventData.element.dataset.viewportIndex, 10);
|
||||
|
||||
const toolContextMenuData = {
|
||||
eventData,
|
||||
isTouchEvent: true,
|
||||
onClose: getOnCloseCallback(viewportIndex),
|
||||
};
|
||||
|
||||
setToolContextMenuData(viewportIndex, toolContextMenuData);
|
||||
};
|
||||
}
|
||||
|
||||
function getResetLabellingAndContextMenu(store) {
|
||||
return function resetLabellingAndContextMenu() {
|
||||
store.dispatch(resetLabellingAndContextMenuAction());
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @param {*} store
|
||||
* @returns
|
||||
*/
|
||||
function getToolLabellingFlowCallback(store) {
|
||||
const setLabellingFlowData = labellingFlowData => {
|
||||
store.dispatch(setLabellingFlowDataAction(labellingFlowData));
|
||||
};
|
||||
|
||||
return function toolLabellingFlowCallback(
|
||||
measurementData,
|
||||
eventData,
|
||||
doneCallback,
|
||||
options = {}
|
||||
) {
|
||||
const updateLabelling = ({ location, response, description }) => {
|
||||
// Update the measurement data with the labelling parameters
|
||||
|
||||
if (location) {
|
||||
measurementData.location = location;
|
||||
}
|
||||
|
||||
measurementData.description = description || '';
|
||||
|
||||
if (response) {
|
||||
measurementData.response = response;
|
||||
}
|
||||
|
||||
updateTableWithNewMeasurementData(measurementData);
|
||||
};
|
||||
|
||||
const labellingDoneCallback = () => {
|
||||
setLabellingFlowData({ visible: false });
|
||||
};
|
||||
|
||||
const labellingFlowData = {
|
||||
visible: true,
|
||||
eventData,
|
||||
measurementData,
|
||||
skipAddLabelButton: options.skipAddLabelButton,
|
||||
editLocation: options.editLocation,
|
||||
editDescription: options.editDescription,
|
||||
editResponse: options.editResponse,
|
||||
editDescriptionOnDialog: options.editDescriptionOnDialog,
|
||||
labellingDoneCallback,
|
||||
updateLabelling,
|
||||
};
|
||||
|
||||
setLabellingFlowData(labellingFlowData);
|
||||
};
|
||||
}
|
||||
|
||||
export {
|
||||
getToolLabellingFlowCallback,
|
||||
getOnRightClickCallback,
|
||||
getOnTouchPressCallback,
|
||||
getResetLabellingAndContextMenu,
|
||||
};
|
||||
Loaded 100 of 131 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user