Compare commits
23
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8c946a4742 | ||
|
|
30b9e44220 | ||
|
|
380abe2705 | ||
|
|
f4570c96c2 | ||
|
|
1fe951a71e | ||
|
|
ebe1af8d4f | ||
|
|
5cda5159da | ||
|
|
115a3ec0b0 | ||
|
|
ac82a111e7 | ||
|
|
a22fd67e13 | ||
|
|
91f0af058f | ||
|
|
5a1186388d | ||
|
|
2f0f92c095 | ||
|
|
5c686268ab | ||
|
|
974aabc9b2 | ||
|
|
17e3b49b06 | ||
|
|
e3c119489a | ||
|
|
c91b6dddd8 | ||
|
|
92548191e4 | ||
|
|
68fcaef19a | ||
|
|
ef611e4849 | ||
|
|
d128fd6bf0 | ||
|
|
3bdd434488 |
No files matched your search
@@ -367,6 +367,31 @@ workflows:
|
|||||||
requires:
|
requires:
|
||||||
- UNIT_TESTS
|
- 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:
|
PR_OPTIONAL_DOCKER_PUBLISH:
|
||||||
jobs:
|
jobs:
|
||||||
# https://circleci.com/docs/2.0/workflows/#holding-a-workflow-for-a-manual-approval
|
# https://circleci.com/docs/2.0/workflows/#holding-a-workflow-for-a-manual-approval
|
||||||
@@ -419,6 +444,27 @@ workflows:
|
|||||||
filters:
|
filters:
|
||||||
branches:
|
branches:
|
||||||
only: master
|
only: master
|
||||||
|
# Update base branch snapshots
|
||||||
|
- 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:
|
- DOCKER_MASTER_PUBLISH:
|
||||||
requires:
|
requires:
|
||||||
- NPM_PUBLISH
|
- NPM_PUBLISH
|
||||||
|
|||||||
@@ -3,6 +3,25 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
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)
|
## [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
|
**Note:** Version bump only for package @ohif/extension-cornerstone
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@ohif/extension-cornerstone",
|
"name": "@ohif/extension-cornerstone",
|
||||||
"version": "2.0.1",
|
"version": "2.1.0",
|
||||||
"description": "OHIF extension for Cornerstone",
|
"description": "OHIF extension for Cornerstone",
|
||||||
"author": "OHIF",
|
"author": "OHIF",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -34,7 +34,7 @@
|
|||||||
"cornerstone-math": "^0.1.8",
|
"cornerstone-math": "^0.1.8",
|
||||||
"cornerstone-tools": "^4.8.0",
|
"cornerstone-tools": "^4.8.0",
|
||||||
"cornerstone-wado-image-loader": "^3.0.0",
|
"cornerstone-wado-image-loader": "^3.0.0",
|
||||||
"dcmjs": "^0.6.1",
|
"dcmjs": "^0.8.2",
|
||||||
"dicom-parser": "^1.8.3",
|
"dicom-parser": "^1.8.3",
|
||||||
"hammerjs": "^2.0.8",
|
"hammerjs": "^2.0.8",
|
||||||
"prop-types": "^15.6.2",
|
"prop-types": "^15.6.2",
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ export default function init({ servicesManager, configuration }) {
|
|||||||
];
|
];
|
||||||
|
|
||||||
/* Add extension tools configuration here. */
|
/* Add extension tools configuration here. */
|
||||||
const extensionToolsConfiguration = {
|
const internalToolsConfig = {
|
||||||
ArrowAnnotate: {
|
ArrowAnnotate: {
|
||||||
configuration: {
|
configuration: {
|
||||||
getTextCallback: (callback, eventDetails) =>
|
getTextCallback: (callback, eventDetails) =>
|
||||||
@@ -113,20 +113,26 @@ export default function init({ servicesManager, configuration }) {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const isEmpty = obj => Object.keys(obj).length < 1;
|
/* Add tools with its custom props through extension configuration. */
|
||||||
if (!isEmpty(configuration.tools) || !isEmpty(extensionToolsConfiguration)) {
|
tools.forEach(tool => {
|
||||||
/* Add tools with its custom props through extension configuration. */
|
const toolName = tool.name.replace('Tool', '');
|
||||||
tools.forEach(tool => {
|
const externalToolsConfig = configuration.tools || {};
|
||||||
const toolName = tool.name.replace('Tool', '');
|
const externalToolProps = externalToolsConfig[toolName] || {};
|
||||||
const configurationToolProps = configuration.tools[toolName] || {};
|
const internalToolProps = internalToolsConfig[toolName] || {};
|
||||||
const extensionToolProps = extensionToolsConfiguration[toolName];
|
const props = merge(internalToolProps, externalToolProps);
|
||||||
let props = merge(extensionToolProps, configurationToolProps);
|
csTools.addTool(tool, props);
|
||||||
csTools.addTool(tool, props);
|
});
|
||||||
});
|
|
||||||
} else {
|
|
||||||
tools.forEach(tool => csTools.addTool(tool));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// 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('Pan', { mouseButtonMask: 4 });
|
||||||
csTools.setToolActive('Zoom', { mouseButtonMask: 2 });
|
csTools.setToolActive('Zoom', { mouseButtonMask: 2 });
|
||||||
csTools.setToolActive('Wwwc', { mouseButtonMask: 1 });
|
csTools.setToolActive('Wwwc', { mouseButtonMask: 1 });
|
||||||
|
|||||||
@@ -3,6 +3,17 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
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)
|
## [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
|
**Note:** Version bump only for package @ohif/extension-dicom-html
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@ohif/extension-dicom-html",
|
"name": "@ohif/extension-dicom-html",
|
||||||
"version": "1.0.2",
|
"version": "1.1.0",
|
||||||
"description": "OHIF extension for rendering structured reports to HTML",
|
"description": "OHIF extension for rendering structured reports to HTML",
|
||||||
"author": "OHIF",
|
"author": "OHIF",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@ohif/core": "^0.50.0",
|
"@ohif/core": "^0.50.0",
|
||||||
"dcmjs": "^0.6.1",
|
"dcmjs": "^0.8.2",
|
||||||
"prop-types": "^15.6.2",
|
"prop-types": "^15.6.2",
|
||||||
"react": "^16.8.6",
|
"react": "^16.8.6",
|
||||||
"react-dom": "^16.8.6"
|
"react-dom": "^16.8.6"
|
||||||
|
|||||||
@@ -3,6 +3,33 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
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)
|
## [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
|
**Note:** Version bump only for package @ohif/extension-vtk
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@ohif/extension-vtk",
|
"name": "@ohif/extension-vtk",
|
||||||
"version": "1.0.2",
|
"version": "1.1.2",
|
||||||
"description": "OHIF extension for VTK.js",
|
"description": "OHIF extension for VTK.js",
|
||||||
"author": "OHIF",
|
"author": "OHIF",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -34,7 +34,7 @@
|
|||||||
"@ohif/ui": "^0.50.0",
|
"@ohif/ui": "^0.50.0",
|
||||||
"cornerstone-core": "^2.2.8",
|
"cornerstone-core": "^2.2.8",
|
||||||
"cornerstone-wado-image-loader": "^3.0.0",
|
"cornerstone-wado-image-loader": "^3.0.0",
|
||||||
"dcmjs": "^0.6.1",
|
"dcmjs": "^0.8.2",
|
||||||
"dicom-parser": "^1.8.3",
|
"dicom-parser": "^1.8.3",
|
||||||
"i18next": "^17.0.3",
|
"i18next": "^17.0.3",
|
||||||
"i18next-browser-languagedetector": "^3.0.1",
|
"i18next-browser-languagedetector": "^3.0.1",
|
||||||
@@ -52,11 +52,10 @@
|
|||||||
"react-vtkjs-viewport": "^0.3.9"
|
"react-vtkjs-viewport": "^0.3.9"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@ohif/core": "^2.0.2",
|
"@ohif/core": "^2.1.0",
|
||||||
"@ohif/ui": "^1.0.1",
|
"@ohif/ui": "^1.1.2",
|
||||||
"cornerstone-tools": "^4.8.0",
|
"cornerstone-tools": "^4.8.0",
|
||||||
"cornerstone-wado-image-loader": "^3.0.0",
|
"cornerstone-wado-image-loader": "^3.0.0",
|
||||||
"dcmjs": "^0.6.1",
|
|
||||||
"dicom-parser": "^1.8.3",
|
"dicom-parser": "^1.8.3",
|
||||||
"gh-pages": "^2.0.1",
|
"gh-pages": "^2.0.1",
|
||||||
"i18next": "^17.0.3",
|
"i18next": "^17.0.3",
|
||||||
|
|||||||
@@ -3,6 +3,17 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
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)
|
## [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
|
**Note:** Version bump only for package @ohif/core
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@ohif/core",
|
"name": "@ohif/core",
|
||||||
"version": "2.0.2",
|
"version": "2.1.0",
|
||||||
"description": "Generic business logic for web-based medical imaging applications",
|
"description": "Generic business logic for web-based medical imaging applications",
|
||||||
"author": "OHIF Core Team",
|
"author": "OHIF Core Team",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import cornerstoneTools from 'cornerstone-tools';
|
import cornerstoneTools from 'cornerstone-tools';
|
||||||
|
import cornerstone from 'cornerstone-core';
|
||||||
import log from '../../log';
|
import log from '../../log';
|
||||||
import getLabel from '../lib/getLabel';
|
import getLabel from '../lib/getLabel';
|
||||||
import getDescription from '../lib/getDescription';
|
import getDescription from '../lib/getDescription';
|
||||||
@@ -7,13 +8,32 @@ import guid from '../../utils/guid';
|
|||||||
import studyMetadataManager from '../../utils/studyMetadataManager';
|
import studyMetadataManager from '../../utils/studyMetadataManager';
|
||||||
import { measurementApiDefaultConfig } from './../configuration.js';
|
import { measurementApiDefaultConfig } from './../configuration.js';
|
||||||
|
|
||||||
|
|
||||||
const configuration = {
|
const configuration = {
|
||||||
...measurementApiDefaultConfig,
|
...measurementApiDefaultConfig
|
||||||
};
|
};
|
||||||
|
|
||||||
export default class MeasurementApi {
|
export default class MeasurementApi {
|
||||||
static Instance;
|
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) {
|
static setConfiguration(config) {
|
||||||
Object.assign(configuration, config);
|
Object.assign(configuration, config);
|
||||||
}
|
}
|
||||||
@@ -203,13 +223,14 @@ export default class MeasurementApi {
|
|||||||
|
|
||||||
retrieveMeasurements(patientId, timepointIds) {
|
retrieveMeasurements(patientId, timepointIds) {
|
||||||
const retrievalFn = configuration.dataExchange.retrieve;
|
const retrievalFn = configuration.dataExchange.retrieve;
|
||||||
|
const { server } = configuration;
|
||||||
if (typeof retrievalFn !== 'function') {
|
if (typeof retrievalFn !== 'function') {
|
||||||
log.error('Measurement retrieval function has not been configured.');
|
log.error('Measurement retrieval function has not been configured.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
retrievalFn(patientId, timepointIds).then(measurementData => {
|
retrievalFn(server).then(measurementData => {
|
||||||
if (measurementData) {
|
if (measurementData) {
|
||||||
log.info('Measurement data retrieval');
|
log.info('Measurement data retrieval');
|
||||||
log.info(measurementData);
|
log.info(measurementData);
|
||||||
@@ -230,6 +251,10 @@ export default class MeasurementApi {
|
|||||||
// Synchronize the new tool data
|
// Synchronize the new tool data
|
||||||
this.syncMeasurementsAndToolData();
|
this.syncMeasurementsAndToolData();
|
||||||
|
|
||||||
|
cornerstone.getEnabledElements().forEach(enabledElement => {
|
||||||
|
cornerstone.updateImage(enabledElement.element);
|
||||||
|
});
|
||||||
|
|
||||||
// Let others know that the measurements are updated
|
// Let others know that the measurements are updated
|
||||||
this.onMeasurementsUpdated();
|
this.onMeasurementsUpdated();
|
||||||
}, reject);
|
}, reject);
|
||||||
@@ -237,6 +262,7 @@ export default class MeasurementApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
storeMeasurements(timepointId) {
|
storeMeasurements(timepointId) {
|
||||||
|
const { server } = configuration;
|
||||||
const storeFn = configuration.dataExchange.store;
|
const storeFn = configuration.dataExchange.store;
|
||||||
if (typeof storeFn !== 'function') {
|
if (typeof storeFn !== 'function') {
|
||||||
log.error('Measurement store function has not been configured.');
|
log.error('Measurement store function has not been configured.');
|
||||||
@@ -278,8 +304,9 @@ export default class MeasurementApi {
|
|||||||
};
|
};
|
||||||
|
|
||||||
log.info('Saving Measurements for timepoints:', timepoints);
|
log.info('Saving Measurements for timepoints:', timepoints);
|
||||||
return storeFn(measurementData, filter).then(() => {
|
return storeFn(measurementData, filter, server).then(result => {
|
||||||
log.info('Measurement storage completed');
|
log.info('Measurement storage completed');
|
||||||
|
return result;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,17 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
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)
|
## [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",
|
"name": "@ohif/i18n",
|
||||||
"version": "0.52.2",
|
"version": "0.52.3",
|
||||||
"description": "Internationalization library for The OHIF Viewer",
|
"description": "Internationalization library for The OHIF Viewer",
|
||||||
"author": "OHIF",
|
"author": "OHIF",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
"Clear": "Clear",
|
"Clear": "Clear",
|
||||||
"Coronal": "Coronal",
|
"Coronal": "Coronal",
|
||||||
"Crosshairs": "Crosshairs",
|
"Crosshairs": "Crosshairs",
|
||||||
|
"Download": "Download",
|
||||||
"Ellipse": "Ellipse",
|
"Ellipse": "Ellipse",
|
||||||
"Elliptical": "Elliptical",
|
"Elliptical": "Elliptical",
|
||||||
"Flip H": "Flip H",
|
"Flip H": "Flip H",
|
||||||
|
|||||||
@@ -10,18 +10,19 @@
|
|||||||
"Clear": "Limpiar",
|
"Clear": "Limpiar",
|
||||||
"Coronal": "Coronal",
|
"Coronal": "Coronal",
|
||||||
"Crosshairs": "Punto de mira",
|
"Crosshairs": "Punto de mira",
|
||||||
|
"Download": "Descargar",
|
||||||
"Ellipse": "Elipse",
|
"Ellipse": "Elipse",
|
||||||
"Elliptical": "Elíptico",
|
"Elliptical": "Elíptico",
|
||||||
"Flip H": "Espejo H",
|
"Flip H": "Voltear H",
|
||||||
"Flip V": "Espejo V",
|
"Flip V": "Voltear V",
|
||||||
"Freehand": "Mano alzada",
|
"Freehand": "Mano alzada",
|
||||||
"Invert": "Invertir",
|
"Invert": "Negativo",
|
||||||
"Layout": "$t(Common:Layout)",
|
"Layout": "$t(Common:Layout)",
|
||||||
"Length": "Longitud",
|
"Length": "Longitud",
|
||||||
"Levels": "Niveles",
|
"Levels": "W/L",
|
||||||
"Magnify": "Aumentar",
|
"Magnify": "Lupa",
|
||||||
"Manual": "Manual",
|
"Manual": "Manual",
|
||||||
"Measurements": "Mediciones",
|
"Measurements": "Medidas",
|
||||||
"More": "$t(Common:More)",
|
"More": "$t(Common:More)",
|
||||||
"Next": "$t(Common:Next)",
|
"Next": "$t(Common:Next)",
|
||||||
"Pan": "Mover",
|
"Pan": "Mover",
|
||||||
@@ -32,11 +33,11 @@
|
|||||||
"Rectangle": "Rectángulo",
|
"Rectangle": "Rectángulo",
|
||||||
"Reset": "$t(Common:Reset)",
|
"Reset": "$t(Common:Reset)",
|
||||||
"Reset to Defaults": "$t(Common:Reset) por defecto",
|
"Reset to Defaults": "$t(Common:Reset) por defecto",
|
||||||
"Rotate Right": "Rotar ->",
|
"Rotate Right": "Girar ->",
|
||||||
"Sagittal": "Sagital",
|
"Sagittal": "Sagital",
|
||||||
"Save": "Guardar",
|
"Save": "Guardar",
|
||||||
"Stack Scroll": "Scroll en pila",
|
"Stack Scroll": "Scroll",
|
||||||
"Stop": "$t(Common:Stop)",
|
"Stop": "$t(Common:Stop)",
|
||||||
"Themes": "Temas",
|
"Themes": "Temas",
|
||||||
"Zoom": "Zoom"
|
"Zoom": "Ampliar"
|
||||||
}
|
}
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"Image": "Imagen",
|
"Image": "Imagen",
|
||||||
"Layout": "Disposición",
|
"Layout": "Formato",
|
||||||
"Measurements": "Medidas",
|
"Measurements": "Medidas",
|
||||||
"More": "Más",
|
"More": "Más",
|
||||||
"Next": "Siguiente",
|
"Next": "Siguiente",
|
||||||
"Play": "Play",
|
"Play": "Play",
|
||||||
"Previous": "Anterior",
|
"Previous": "Anterior",
|
||||||
"Reset": "Resetear",
|
"Reset": "Restaurar",
|
||||||
"RowsPerPage": "filas por página",
|
"RowsPerPage": "filas por página",
|
||||||
"Series": "Secuencia",
|
"Series": "Secuencia",
|
||||||
"Show": "Mostrar",
|
"Show": "Mostrar",
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
"Clear": "Limpar",
|
"Clear": "Limpar",
|
||||||
"Coronal": "Coronal",
|
"Coronal": "Coronal",
|
||||||
"Crosshairs": "Localizador",
|
"Crosshairs": "Localizador",
|
||||||
|
"Download": "Baixar",
|
||||||
"Ellipse": "Elipse",
|
"Ellipse": "Elipse",
|
||||||
"Elliptical": "Elíptico",
|
"Elliptical": "Elíptico",
|
||||||
"Flip H": "Inverter H",
|
"Flip H": "Inverter H",
|
||||||
@@ -30,8 +31,8 @@
|
|||||||
"Probe": "Prova",
|
"Probe": "Prova",
|
||||||
"ROI Window": "Janela ROI",
|
"ROI Window": "Janela ROI",
|
||||||
"Rectangle": "Retângulo",
|
"Rectangle": "Retângulo",
|
||||||
"Reset": "Resetar",
|
"Reset": "$t(Common:Reset)",
|
||||||
"Reset to Defaults": "Restaurar para o Padrão",
|
"Reset to Defaults": "$t(Common:Reset) para o Padrão",
|
||||||
"Rotate Right": "Girar à direita",
|
"Rotate Right": "Girar à direita",
|
||||||
"Sagittal": "Sagital",
|
"Sagittal": "Sagital",
|
||||||
"Save": "Salvar",
|
"Save": "Salvar",
|
||||||
|
|||||||
@@ -6,6 +6,6 @@
|
|||||||
"Next": "Próximo",
|
"Next": "Próximo",
|
||||||
"Play": "Play",
|
"Play": "Play",
|
||||||
"Previous": "Anterior",
|
"Previous": "Anterior",
|
||||||
"Reset": "Reset",
|
"Reset": "Restaurar",
|
||||||
"Stop": "Stop"
|
"Stop": "Stop"
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"Cancel": "Cancelar",
|
"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.",
|
"ResetDefaultMessage": "Preferências resetadas com sucesso. <br /> Você deve <strong>Salvar</strong> para que essa ação seja realizada.",
|
||||||
"Save": "Salvar",
|
"Save": "Salvar",
|
||||||
"SaveMessage": "Preferências salvas",
|
"SaveMessage": "Preferências salvas",
|
||||||
|
|||||||
@@ -3,6 +3,36 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
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)
|
## [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
|
**Note:** Version bump only for package @ohif/ui
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@ohif/ui",
|
"name": "@ohif/ui",
|
||||||
"version": "1.0.1",
|
"version": "1.1.2",
|
||||||
"description": "A set of React components for Medical Imaging Viewers",
|
"description": "A set of React components for Medical Imaging Viewers",
|
||||||
"author": "OHIF Contributors",
|
"author": "OHIF Contributors",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ const ViewportDownloadForm = ({
|
|||||||
maximumSize,
|
maximumSize,
|
||||||
canvasClass,
|
canvasClass,
|
||||||
}) => {
|
}) => {
|
||||||
const [t] = useTranslation('ViewportDownloadForm');
|
const [t] = useTranslation('Buttons');
|
||||||
|
|
||||||
const [filename, setFilename] = useState(DEFAULT_FILENAME);
|
const [filename, setFilename] = useState(DEFAULT_FILENAME);
|
||||||
const [fileType, setFileType] = useState('jpg');
|
const [fileType, setFileType] = useState('jpg');
|
||||||
@@ -196,11 +196,12 @@ const ViewportDownloadForm = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="file-info-container">
|
<div className="file-info-container" data-cy="file-info-container">
|
||||||
<div className="col">
|
<div className="col">
|
||||||
<div className="width">
|
<div className="width">
|
||||||
<TextInput
|
<TextInput
|
||||||
type="number"
|
type="number"
|
||||||
|
data-cy="image-width"
|
||||||
min={minimumSize}
|
min={minimumSize}
|
||||||
max={maximumSize}
|
max={maximumSize}
|
||||||
value={width}
|
value={width}
|
||||||
@@ -211,6 +212,7 @@ const ViewportDownloadForm = ({
|
|||||||
<div className="height">
|
<div className="height">
|
||||||
<TextInput
|
<TextInput
|
||||||
type="number"
|
type="number"
|
||||||
|
data-cy="image-height"
|
||||||
min={minimumSize}
|
min={minimumSize}
|
||||||
max={maximumSize}
|
max={maximumSize}
|
||||||
value={height}
|
value={height}
|
||||||
@@ -224,6 +226,7 @@ const ViewportDownloadForm = ({
|
|||||||
<div className="file-name">
|
<div className="file-name">
|
||||||
<TextInput
|
<TextInput
|
||||||
type="text"
|
type="text"
|
||||||
|
data-cy="file-name"
|
||||||
value={filename}
|
value={filename}
|
||||||
onChange={event => setFilename(event.target.value)}
|
onChange={event => setFilename(event.target.value)}
|
||||||
label={t('File name')}
|
label={t('File name')}
|
||||||
@@ -233,6 +236,7 @@ const ViewportDownloadForm = ({
|
|||||||
<div className="file-type">
|
<div className="file-type">
|
||||||
<Select
|
<Select
|
||||||
value={fileType}
|
value={fileType}
|
||||||
|
data-cy="file-type"
|
||||||
onChange={event => setFileType(event.target.value)}
|
onChange={event => setFileType(event.target.value)}
|
||||||
options={FILE_TYPE_OPTIONS}
|
options={FILE_TYPE_OPTIONS}
|
||||||
label={t('File type')}
|
label={t('File type')}
|
||||||
@@ -245,6 +249,7 @@ const ViewportDownloadForm = ({
|
|||||||
<label htmlFor="show-annotations" className="form-check-label">
|
<label htmlFor="show-annotations" className="form-check-label">
|
||||||
<input
|
<input
|
||||||
id="show-annotations"
|
id="show-annotations"
|
||||||
|
data-cy="show-annotations"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
className="form-check-input"
|
className="form-check-input"
|
||||||
checked={showAnnotations}
|
checked={showAnnotations}
|
||||||
@@ -278,23 +283,33 @@ const ViewportDownloadForm = ({
|
|||||||
></canvas>
|
></canvas>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="preview">
|
<div className="preview" data-cy="image-preview">
|
||||||
<h4> {t('Image Preview')}</h4>
|
<h4> {t('Image Preview')}</h4>
|
||||||
<img
|
<img
|
||||||
className="viewport-preview"
|
className="viewport-preview"
|
||||||
src={viewportPreview.src}
|
src={viewportPreview.src}
|
||||||
alt="Viewport Preview"
|
alt="Viewport Preview"
|
||||||
|
data-cy="viewport-preview-img"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="actions">
|
<div className="actions">
|
||||||
<div className="action-cancel">
|
<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')}
|
{t('Cancel')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="action-save">
|
<div className="action-save">
|
||||||
<button onClick={downloadImage} className="btn btn-primary">
|
<button
|
||||||
|
onClick={downloadImage}
|
||||||
|
className="btn btn-primary"
|
||||||
|
data-cy="download-btn"
|
||||||
|
>
|
||||||
{t('Download')}
|
{t('Download')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -22,8 +22,9 @@ class MeasurementTable extends Component {
|
|||||||
onDeleteClick: PropTypes.func,
|
onDeleteClick: PropTypes.func,
|
||||||
onEditDescriptionClick: PropTypes.func,
|
onEditDescriptionClick: PropTypes.func,
|
||||||
selectedMeasurementNumber: PropTypes.number,
|
selectedMeasurementNumber: PropTypes.number,
|
||||||
overwallWarnings: PropTypes.object,
|
|
||||||
t: PropTypes.func,
|
t: PropTypes.func,
|
||||||
|
saveFunction: PropTypes.func,
|
||||||
|
onSaveComplete: PropTypes.func,
|
||||||
};
|
};
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
@@ -38,8 +39,9 @@ class MeasurementTable extends Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const hasOverallWarnings =
|
const { overallWarnings, saveFunction, t } = this.props;
|
||||||
this.props.overallWarnings.warningList.length > 0;
|
const hasOverallWarnings = overallWarnings.warningList.length > 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="measurementTable">
|
<div className="measurementTable">
|
||||||
<div className="measurementTableHeader">
|
<div className="measurementTableHeader">
|
||||||
@@ -55,7 +57,7 @@ class MeasurementTable extends Component {
|
|||||||
style={{}}
|
style={{}}
|
||||||
>
|
>
|
||||||
<div className="warningTitle">
|
<div className="warningTitle">
|
||||||
{this.props.t('Criteria nonconformities')}
|
{t('Criteria nonconformities')}
|
||||||
</div>
|
</div>
|
||||||
<div className="warningContent">
|
<div className="warningContent">
|
||||||
{this.getWarningContent()}
|
{this.getWarningContent()}
|
||||||
@@ -75,10 +77,42 @@ class MeasurementTable extends Component {
|
|||||||
<ScrollableArea>
|
<ScrollableArea>
|
||||||
<div>{this.getMeasurementsGroups()}</div>
|
<div>{this.getMeasurementsGroups()}</div>
|
||||||
</ScrollableArea>
|
</ScrollableArea>
|
||||||
|
<div className="measurementTableFooter">
|
||||||
|
{saveFunction && (
|
||||||
|
<button onClick={this.saveFunction} className="saveBtn">
|
||||||
|
<Icon name="save" width="14px" height="14px" />
|
||||||
|
Save measurements
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</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 = () => {
|
getMeasurementsGroups = () => {
|
||||||
return this.props.measurementCollection.map((measureGroup, index) => {
|
return this.props.measurementCollection.map((measureGroup, index) => {
|
||||||
return (
|
return (
|
||||||
@@ -157,7 +191,7 @@ class MeasurementTable extends Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
getWarningContent = () => {
|
getWarningContent = () => {
|
||||||
const { warningList = '' } = this.props.overwallWarnings;
|
const { warningList = '' } = this.props.overallWarnings;
|
||||||
|
|
||||||
if (Array.isArray(warningList)) {
|
if (Array.isArray(warningList)) {
|
||||||
const listedWarnings = warningList.map((warn, index) => {
|
const listedWarnings = warningList.map((warn, index) => {
|
||||||
|
|||||||
@@ -1,92 +1,117 @@
|
|||||||
.measurementTable
|
.measurementTable
|
||||||
background-color: var(--primary-background-color)
|
background-color: var(--primary-background-color)
|
||||||
height: 100%;
|
height: 100%
|
||||||
width: 100%;
|
width: 100%
|
||||||
display: flex;
|
display: flex
|
||||||
flex-direction: column;
|
flex-direction: column
|
||||||
|
|
||||||
.maxMeasurements
|
.maxMeasurements
|
||||||
border-radius: 3px;
|
border-radius: 3px
|
||||||
color: var(--primary-background-color);
|
color: var(--primary-background-color)
|
||||||
font-size: 12px;
|
font-size: 12px
|
||||||
font-weight: 500;
|
font-weight: 500
|
||||||
height: 19px;
|
height: 19px
|
||||||
line-height: 17px;
|
line-height: 17px
|
||||||
margin-top: 22px;
|
margin-top: 22px
|
||||||
margin-right: 15px;
|
margin-right: 15px
|
||||||
padding: 2px 6px 0;
|
padding: 2px 6px 0
|
||||||
text-transform: uppercase;
|
text-transform: uppercase
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease
|
||||||
background-color: var(--default-color);
|
background-color: var(--default-color)
|
||||||
|
|
||||||
.measurementTableHeader
|
.measurementTableHeader
|
||||||
padding-left: 45px;
|
padding-left: 45px
|
||||||
|
|
||||||
.measurementTableHeaderItem
|
.measurementTableHeaderItem
|
||||||
display: inline-block;
|
display: inline-block
|
||||||
margin-top: 9px;
|
margin-top: 9px
|
||||||
margin-left: 9px;
|
margin-left: 9px
|
||||||
padding-left: 9px;
|
padding-left: 9px
|
||||||
width: 90px;
|
width: 90px
|
||||||
font-size: 14px;
|
font-size: 14px
|
||||||
line-height: 20px;
|
line-height: 20px
|
||||||
border-left: 1px solid var(--text-secondary-color);
|
border-left: 1px solid var(--text-secondary-color)
|
||||||
color: var(--text-primary-color);
|
color: var(--text-primary-color)
|
||||||
|
|
||||||
.timepointLabel
|
.timepointLabel
|
||||||
color: var(--text-secondary-color);
|
color: var(--text-secondary-color)
|
||||||
font-size: 12px;
|
font-size: 12px
|
||||||
|
|
||||||
.warning-status
|
.warning-status
|
||||||
display: block;
|
display: block
|
||||||
position: absolute;
|
position: absolute
|
||||||
width: 50px;
|
width: 50px
|
||||||
height: 40px;
|
height: 40px
|
||||||
padding-left: 7px;
|
padding-left: 7px
|
||||||
left: 15px;
|
left: 15px
|
||||||
top: 7px;
|
top: 7px
|
||||||
|
|
||||||
.warning-border
|
.warning-border
|
||||||
display: block;
|
display: block
|
||||||
border: 2px solid #e29e4a;
|
border: 2px solid #e29e4a
|
||||||
border-radius: 36px;
|
border-radius: 36px
|
||||||
padding: 6px 7px;
|
padding: 6px 7px
|
||||||
width: 40px;
|
width: 40px
|
||||||
height: 40px;
|
height: 40px
|
||||||
|
|
||||||
svg
|
svg
|
||||||
color: #e29e4a;
|
color: #e29e4a
|
||||||
width: 22px;
|
width: 22px
|
||||||
height: 20px;
|
height: 20px
|
||||||
pointer-events: inherit;
|
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
|
.tooltip-warning
|
||||||
&.in
|
&.in
|
||||||
opacity 1;
|
opacity 1
|
||||||
|
|
||||||
.tooltip-inner
|
.tooltip-inner
|
||||||
padding: 0
|
padding: 0
|
||||||
|
|
||||||
.warningTitle
|
.warningTitle
|
||||||
text-align: left
|
text-align: left
|
||||||
background-color: var(--ui-gray-darkest);
|
background-color: var(--ui-gray-darkest)
|
||||||
padding: 7px 15px;
|
padding: 7px 15px
|
||||||
border-top-left-radius: 7px;
|
border-top-left-radius: 7px
|
||||||
border-top-right-radius: 7px;
|
border-top-right-radius: 7px
|
||||||
|
|
||||||
.warningContent
|
.warningContent
|
||||||
text-align center
|
text-align center
|
||||||
background-color: var(--ui-gray);
|
background-color: var(--ui-gray)
|
||||||
border-bottom-left-radius: 7px;
|
border-bottom-left-radius: 7px
|
||||||
border-bottom-right-radius: 7px;
|
border-bottom-right-radius: 7px
|
||||||
|
|
||||||
ol
|
ol
|
||||||
margin-top: auto;
|
margin-top: auto
|
||||||
padding-inline-start: 20px;
|
padding-inline-start: 20px
|
||||||
padding-top: 10px;
|
padding-top: 10px
|
||||||
padding-bottom: 10px;
|
padding-bottom: 10px
|
||||||
padding-left: 25px;
|
padding-left: 25px
|
||||||
text-align: left;
|
text-align: left
|
||||||
|
|
||||||
li
|
li
|
||||||
padding-right: 10px;
|
padding-right: 10px
|
||||||
@@ -29,7 +29,7 @@ const OHIFModal = ({
|
|||||||
const renderHeader = () => {
|
const renderHeader = () => {
|
||||||
return (
|
return (
|
||||||
title && (
|
title && (
|
||||||
<div className="OHIFModal__header">
|
<div className="OHIFModal__header" data-cy="modal-header">
|
||||||
<h4>{title}</h4>
|
<h4>{title}</h4>
|
||||||
{closeButton && (
|
{closeButton && (
|
||||||
<button data-cy="close-button" onClick={onClose}>
|
<button data-cy="close-button" onClick={onClose}>
|
||||||
@@ -44,6 +44,7 @@ const OHIFModal = ({
|
|||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
className={classNames('OHIFModal', className)}
|
className={classNames('OHIFModal', className)}
|
||||||
|
data-cy="modal"
|
||||||
shouldCloseOnEsc={shouldCloseOnEsc}
|
shouldCloseOnEsc={shouldCloseOnEsc}
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
title={title}
|
title={title}
|
||||||
@@ -51,7 +52,9 @@ const OHIFModal = ({
|
|||||||
>
|
>
|
||||||
<>
|
<>
|
||||||
{renderHeader()}
|
{renderHeader()}
|
||||||
<div className="OHIFModal__content">{children}</div>
|
<div className="OHIFModal__content" data-cy="modal-content">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ import powerOff from './icons/power-off.svg';
|
|||||||
import reset from './icons/reset.svg';
|
import reset from './icons/reset.svg';
|
||||||
import rotate from './icons/rotate.svg';
|
import rotate from './icons/rotate.svg';
|
||||||
import rotateRight from './icons/rotate-right.svg';
|
import rotateRight from './icons/rotate-right.svg';
|
||||||
|
import saveRegular from './icons/save-regular.svg';
|
||||||
import search from './icons/search.svg';
|
import search from './icons/search.svg';
|
||||||
import searchPlus from './icons/search-plus.svg';
|
import searchPlus from './icons/search-plus.svg';
|
||||||
import softTissue from './icons/soft-tissue.svg';
|
import softTissue from './icons/soft-tissue.svg';
|
||||||
@@ -164,6 +165,7 @@ const ICONS = {
|
|||||||
'soft-tissue': softTissue,
|
'soft-tissue': softTissue,
|
||||||
lung,
|
lung,
|
||||||
liver,
|
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 |
@@ -3,6 +3,41 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
## [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)
|
## [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
|
**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', () => {
|
it('uses hotkeys "RightArrow" and "LeftArrow" to navigate between multiple viewports', () => {
|
||||||
//Click on Layout button
|
//Select viewport layout (3,1)
|
||||||
cy.get('@layoutBtn').click();
|
cy.setLayout(3, 1);
|
||||||
//Select 3 viewports
|
|
||||||
cy.get('tbody > :nth-child(1) > :nth-child(3)').click();
|
|
||||||
cy.waitViewportImageLoading();
|
cy.waitViewportImageLoading();
|
||||||
|
|
||||||
// Press multiples hotkeys on viewport #1
|
// 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');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -11,6 +11,7 @@ export function initCornerstoneToolsAliases() {
|
|||||||
cy.get('.ToolbarRow > :nth-child(10)').as('cineBtn');
|
cy.get('.ToolbarRow > :nth-child(10)').as('cineBtn');
|
||||||
cy.get('.expandableToolMenu').as('moreBtn');
|
cy.get('.expandableToolMenu').as('moreBtn');
|
||||||
cy.get('.btn-group > .toolbar-button').as('layoutBtn');
|
cy.get('.btn-group > .toolbar-button').as('layoutBtn');
|
||||||
|
cy.get('.viewport-element').as('viewport');
|
||||||
}
|
}
|
||||||
|
|
||||||
//Creating aliases for Common page elements
|
//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) => {
|
Cypress.Commands.add('setLanguage', (language, save = true) => {
|
||||||
cy.openPreferences();
|
cy.openPreferences();
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@ohif/viewer",
|
"name": "@ohif/viewer",
|
||||||
"version": "3.0.2",
|
"version": "3.1.2",
|
||||||
"description": "OHIF Viewer",
|
"description": "OHIF Viewer",
|
||||||
"author": "OHIF Contributors",
|
"author": "OHIF Contributors",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -45,14 +45,14 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.5.5",
|
"@babel/runtime": "^7.5.5",
|
||||||
"@ohif/core": "^2.0.2",
|
"@ohif/core": "^2.1.0",
|
||||||
"@ohif/extension-cornerstone": "^2.0.1",
|
"@ohif/extension-cornerstone": "^2.1.0",
|
||||||
"@ohif/extension-dicom-html": "^1.0.2",
|
"@ohif/extension-dicom-html": "^1.1.0",
|
||||||
"@ohif/extension-dicom-microscopy": "^0.50.6",
|
"@ohif/extension-dicom-microscopy": "^0.50.6",
|
||||||
"@ohif/extension-dicom-pdf": "^1.0.0",
|
"@ohif/extension-dicom-pdf": "^1.0.0",
|
||||||
"@ohif/extension-vtk": "^1.0.2",
|
"@ohif/extension-vtk": "^1.1.2",
|
||||||
"@ohif/i18n": "^0.52.2",
|
"@ohif/i18n": "^0.52.3",
|
||||||
"@ohif/ui": "^1.0.1",
|
"@ohif/ui": "^1.1.2",
|
||||||
"@tanem/react-nprogress": "^1.1.25",
|
"@tanem/react-nprogress": "^1.1.25",
|
||||||
"classnames": "^2.2.6",
|
"classnames": "^2.2.6",
|
||||||
"core-js": "^3.2.1",
|
"core-js": "^3.2.1",
|
||||||
@@ -60,7 +60,7 @@
|
|||||||
"cornerstone-math": "^0.1.8",
|
"cornerstone-math": "^0.1.8",
|
||||||
"cornerstone-tools": "^4.8.0",
|
"cornerstone-tools": "^4.8.0",
|
||||||
"cornerstone-wado-image-loader": "^3.0.0",
|
"cornerstone-wado-image-loader": "^3.0.0",
|
||||||
"dcmjs": "^0.6.1",
|
"dcmjs": "^0.8.2",
|
||||||
"dicom-parser": "^1.8.3",
|
"dicom-parser": "^1.8.3",
|
||||||
"dicomweb-client": "^0.4.4",
|
"dicomweb-client": "^0.4.4",
|
||||||
"hammerjs": "^2.0.8",
|
"hammerjs": "^2.0.8",
|
||||||
|
|||||||
@@ -70,5 +70,5 @@ window.config = {
|
|||||||
// ~ Cornerstone Tools
|
// ~ Cornerstone Tools
|
||||||
{ commandName: 'setZoomTool', label: 'Zoom', keys: ['z'] },
|
{ commandName: 'setZoomTool', label: 'Zoom', keys: ['z'] },
|
||||||
],
|
],
|
||||||
cornerstoneExtensionConfig: { tools: {} },
|
cornerstoneExtensionConfig: {},
|
||||||
};
|
};
|
||||||
@@ -9,7 +9,7 @@ import { hot } from 'react-hot-loader/root';
|
|||||||
import OHIFCornerstoneExtension from '@ohif/extension-cornerstone';
|
import OHIFCornerstoneExtension from '@ohif/extension-cornerstone';
|
||||||
|
|
||||||
import ToolContextMenu from './connectedComponents/ToolContextMenu';
|
import ToolContextMenu from './connectedComponents/ToolContextMenu';
|
||||||
import LabellingManager from './components/Labelling/LabellingManager';
|
import LabellingFlow from './components/Labelling/LabellingFlow';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
SnackbarProvider,
|
SnackbarProvider,
|
||||||
@@ -183,7 +183,7 @@ class App extends Component {
|
|||||||
>
|
>
|
||||||
<LabellingFlowProvider
|
<LabellingFlowProvider
|
||||||
service={UILabellingFlowService}
|
service={UILabellingFlowService}
|
||||||
labellingComponent={LabellingManager}
|
labellingComponent={LabellingFlow}
|
||||||
commandsManager={commandsManager}
|
commandsManager={commandsManager}
|
||||||
>
|
>
|
||||||
<ContextMenuProvider
|
<ContextMenuProvider
|
||||||
@@ -220,7 +220,7 @@ class App extends Component {
|
|||||||
<ModalProvider modal={OHIFModal} service={UIModalService}>
|
<ModalProvider modal={OHIFModal} service={UIModalService}>
|
||||||
<LabellingFlowProvider
|
<LabellingFlowProvider
|
||||||
service={UILabellingFlowService}
|
service={UILabellingFlowService}
|
||||||
labellingComponent={LabellingManager}
|
labellingComponent={LabellingFlow}
|
||||||
commandsManager={commandsManager}
|
commandsManager={commandsManager}
|
||||||
>
|
>
|
||||||
<ContextMenuProvider
|
<ContextMenuProvider
|
||||||
|
|||||||
@@ -3,12 +3,20 @@ import { MeasurementTable } from '@ohif/ui';
|
|||||||
import OHIF from '@ohif/core';
|
import OHIF from '@ohif/core';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import cornerstone from 'cornerstone-core';
|
import cornerstone from 'cornerstone-core';
|
||||||
|
import DICOMSR from './../../lib/DICOMSR';
|
||||||
|
|
||||||
import jumpToRowItem from './jumpToRowItem.js';
|
import jumpToRowItem from './jumpToRowItem.js';
|
||||||
|
|
||||||
const { setViewportSpecificData } = OHIF.redux.actions;
|
const { setViewportSpecificData } = OHIF.redux.actions;
|
||||||
const { MeasurementApi } = OHIF.measurements;
|
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) {
|
function groupBy(list, props) {
|
||||||
return list.reduce((a, b) => {
|
return list.reduce((a, b) => {
|
||||||
(a[b[props]] = a[b[props]] || []).push(b);
|
(a[b[props]] = a[b[props]] || []).push(b);
|
||||||
@@ -16,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 = [];
|
let tools = [];
|
||||||
config.measurementTools.forEach(
|
toolGroups.forEach(
|
||||||
toolGroup => (tools = tools.concat(toolGroup.childTools))
|
toolGroup => (tools = tools.concat(toolGroup.childTools))
|
||||||
);
|
);
|
||||||
|
|
||||||
return tools;
|
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 = {}) {
|
function getMeasurementText(measurementData = {}) {
|
||||||
const defaultText = '...';
|
const defaultText = '...';
|
||||||
const { location = '', description = '' } = measurementData;
|
const { location = '', description = '' } = measurementData;
|
||||||
@@ -34,6 +55,14 @@ function getMeasurementText(measurementData = {}) {
|
|||||||
return result || defaultText;
|
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(
|
function getDataForEachMeasurementNumber(
|
||||||
measurementNumberList,
|
measurementNumberList,
|
||||||
timepoints,
|
timepoints,
|
||||||
@@ -56,10 +85,35 @@ function getDataForEachMeasurementNumber(
|
|||||||
return data;
|
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) {
|
function convertMeasurementsToTableData(toolCollections, timepoints) {
|
||||||
const config = OHIF.measurements.MeasurementApi.getConfiguration();
|
const config = OHIF.measurements.MeasurementApi.getConfiguration();
|
||||||
const toolGroups = config.measurementTools;
|
const toolGroups = config.measurementTools;
|
||||||
const tools = getAllTools();
|
const tools = getAllTools(toolGroups);
|
||||||
|
|
||||||
const tableMeasurements = toolGroups.map(toolGroup => {
|
const tableMeasurements = toolGroups.map(toolGroup => {
|
||||||
return {
|
return {
|
||||||
@@ -88,6 +142,12 @@ function convertMeasurementsToTableData(toolCollections, timepoints) {
|
|||||||
} = measurementData;
|
} = measurementData;
|
||||||
const measurementId = measurementData._id;
|
const measurementId = measurementData._id;
|
||||||
|
|
||||||
|
const {
|
||||||
|
hasWarnings,
|
||||||
|
warningTitle,
|
||||||
|
warningList,
|
||||||
|
} = getWarningsForMeasurement(toolType);
|
||||||
|
|
||||||
//check if all measurements with same measurementNumber will have same LABEL
|
//check if all measurements with same measurementNumber will have same LABEL
|
||||||
const tableMeasurement = {
|
const tableMeasurement = {
|
||||||
itemNumber: lesionNamingNumber,
|
itemNumber: lesionNamingNumber,
|
||||||
@@ -96,10 +156,10 @@ function convertMeasurementsToTableData(toolCollections, timepoints) {
|
|||||||
measurementNumber,
|
measurementNumber,
|
||||||
lesionNamingNumber,
|
lesionNamingNumber,
|
||||||
toolType,
|
toolType,
|
||||||
hasWarnings: false, //TODO
|
hasWarnings,
|
||||||
warningTitle: '', //TODO
|
warningTitle,
|
||||||
|
warningList,
|
||||||
isSplitLesion: false, //TODO
|
isSplitLesion: false, //TODO
|
||||||
warningList: [], //TODO
|
|
||||||
data: getDataForEachMeasurementNumber(
|
data: getDataForEachMeasurementNumber(
|
||||||
measurementNumberList,
|
measurementNumberList,
|
||||||
timepoints,
|
timepoints,
|
||||||
@@ -126,6 +186,13 @@ function convertMeasurementsToTableData(toolCollections, timepoints) {
|
|||||||
return tableMeasurements;
|
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) {
|
function convertTimepointsToTableData(timepoints) {
|
||||||
if (!timepoints || !timepoints.length) {
|
if (!timepoints || !timepoints.length) {
|
||||||
return [];
|
return [];
|
||||||
@@ -140,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 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 {
|
return {
|
||||||
timepoints: convertTimepointsToTableData(timepoints),
|
timepoints: convertTimepointsToTableData(timepoints),
|
||||||
measurementCollection: convertMeasurementsToTableData(
|
measurementCollection: convertMeasurementsToTableData(
|
||||||
@@ -150,6 +237,7 @@ const mapStateToProps = state => {
|
|||||||
),
|
),
|
||||||
timepointManager: state.timepointManager,
|
timepointManager: state.timepointManager,
|
||||||
viewports: state.viewports,
|
viewports: state.viewports,
|
||||||
|
saveFunction,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -265,10 +353,15 @@ const mapDispatchToProps = (dispatch, ownProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const mergeProps = (propsFromState, propsFromDispatch, ownProps) => {
|
const mergeProps = (propsFromState, propsFromDispatch, ownProps) => {
|
||||||
|
const { timepoints, saveFunction, measurementCollection } = propsFromState;
|
||||||
|
const { onSaveComplete, selectedMeasurementNumber } = ownProps;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
timepoints: propsFromState.timepoints,
|
timepoints,
|
||||||
measurementCollection: propsFromState.measurementCollection,
|
saveFunction,
|
||||||
selectedMeasurementNumber: ownProps.selectedMeasurementNumber,
|
measurementCollection,
|
||||||
|
onSaveComplete,
|
||||||
|
selectedMeasurementNumber,
|
||||||
...propsFromDispatch,
|
...propsFromDispatch,
|
||||||
onItemClick: (event, measurementData) => {
|
onItemClick: (event, measurementData) => {
|
||||||
// TODO: Add timepointId to .data for measurementData?
|
// TODO: Add timepointId to .data for measurementData?
|
||||||
|
|||||||
@@ -11,8 +11,9 @@ export default {
|
|||||||
preRegistration({ servicesManager, commandsManager, configuration = {} }) {
|
preRegistration({ servicesManager, commandsManager, configuration = {} }) {
|
||||||
init({ servicesManager, commandsManager, configuration });
|
init({ servicesManager, commandsManager, configuration });
|
||||||
},
|
},
|
||||||
|
|
||||||
getPanelModule({ servicesManager, commandsManager }) {
|
getPanelModule({ servicesManager, commandsManager }) {
|
||||||
const { UILabellingFlowService } = servicesManager.services;
|
const { UILabellingFlowService, UINotificationService } = servicesManager.services;
|
||||||
const ExtendedConnectedMeasurementTable = () => (
|
const ExtendedConnectedMeasurementTable = () => (
|
||||||
<ConnectedMeasurementTable
|
<ConnectedMeasurementTable
|
||||||
onRelabel={tool => {
|
onRelabel={tool => {
|
||||||
@@ -38,6 +39,11 @@ export default {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
onSaveComplete={message => {
|
||||||
|
if (UINotificationService) {
|
||||||
|
UINotificationService.show(message);
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
|
|||||||
+3
-6
@@ -6,8 +6,7 @@
|
|||||||
max-height: 500px;
|
max-height: 500px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.labellingComponent .selectedLabel,
|
.labellingComponent .selectedLabel, .labellingComponent .selectedDescription {
|
||||||
.labellingComponent .selectedDescription {
|
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
background-color: white;
|
background-color: white;
|
||||||
width: 150px;
|
width: 150px;
|
||||||
@@ -86,14 +85,12 @@
|
|||||||
color: #337ab7;
|
color: #337ab7;
|
||||||
}
|
}
|
||||||
|
|
||||||
.labellingComponent .commonButtons,
|
.labellingComponent .commonButtons, .labellingComponent.editDescription .editDescriptionButtons {
|
||||||
.labellingComponent.editDescription .editDescriptionButtons {
|
|
||||||
display: block;
|
display: block;
|
||||||
margin-left: 55px;
|
margin-left: 55px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.labellingComponent.editDescription .commonButtons,
|
.labellingComponent.editDescription .commonButtons, .labellingComponent .editDescriptionButtons {
|
||||||
.labellingComponent .editDescriptionButtons {
|
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,112 +1,165 @@
|
|||||||
import { Icon, SelectTree } from '@ohif/ui';
|
import { Icon, SelectTree } from '@ohif/ui';
|
||||||
import React, { Component } from 'react';
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import cloneDeep from 'lodash.clonedeep';
|
import cloneDeep from 'lodash.clonedeep';
|
||||||
|
|
||||||
import LabellingTransition from './LabellingTransition.js';
|
import LabellingTransition from './LabellingTransition.js';
|
||||||
import OHIFLabellingData from './OHIFLabellingData.js';
|
import OHIFLabellingData from './OHIFLabellingData.js';
|
||||||
|
import EditDescriptionDialog from './../EditDescriptionDialog/EditDescriptionDialog.js';
|
||||||
|
import './LabellingFlow.css';
|
||||||
|
|
||||||
export default class LabellingFlow extends Component {
|
const LabellingFlow = ({
|
||||||
static propTypes = {
|
measurementData,
|
||||||
measurementData: PropTypes.object.isRequired,
|
editLocation,
|
||||||
labellingDoneCallback: PropTypes.func.isRequired,
|
editDescription,
|
||||||
updateLabelling: PropTypes.func.isRequired,
|
skipAddLabelButton,
|
||||||
initialTopDistance: PropTypes.number,
|
updateLabelling,
|
||||||
skipAddLabelButton: PropTypes.bool,
|
labellingDoneCallback,
|
||||||
editLocation: PropTypes.bool,
|
editDescriptionOnDialog,
|
||||||
editDescription: PropTypes.bool,
|
}) => {
|
||||||
|
const [fadeOutTimer, setFadeOutTimer] = useState();
|
||||||
|
const [showComponent, setShowComponent] = useState(true);
|
||||||
|
const descriptionInput = useRef();
|
||||||
|
const [state, setState] = useState({
|
||||||
|
measurementData,
|
||||||
|
editLocation,
|
||||||
|
editDescription,
|
||||||
|
skipAddLabelButton,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const newMeasurementData = cloneDeep(measurementData);
|
||||||
|
|
||||||
|
if (editDescription) {
|
||||||
|
newMeasurementData.description = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (editLocation) {
|
||||||
|
newMeasurementData.location = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
let newEditLocation = editLocation;
|
||||||
|
if (!editDescription && !editLocation) {
|
||||||
|
newEditLocation = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(state => ({
|
||||||
|
...state,
|
||||||
|
editLocation: newEditLocation,
|
||||||
|
measurementData: newMeasurementData,
|
||||||
|
}));
|
||||||
|
}, [editDescription, editLocation, measurementData]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (descriptionInput.current) {
|
||||||
|
descriptionInput.current.focus();
|
||||||
|
}
|
||||||
|
}, [state]);
|
||||||
|
|
||||||
|
const relabel = event =>
|
||||||
|
setState(state => ({ ...state, editLocation: true }));
|
||||||
|
|
||||||
|
const setDescriptionUpdateMode = () => {
|
||||||
|
descriptionInput.current.focus();
|
||||||
|
setState(state => ({ ...state, editDescription: true }));
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(props) {
|
const descriptionCancel = () => {
|
||||||
super(props);
|
const { description = '' } = cloneDeep(state);
|
||||||
|
descriptionInput.current.value = description;
|
||||||
|
setState(state => ({ ...state, editDescription: false }));
|
||||||
|
};
|
||||||
|
|
||||||
const { location, locationLabel, description } = props.measurementData;
|
const handleKeyPress = event => {
|
||||||
|
if (event.key === 'Enter') {
|
||||||
this.state = {
|
descriptionSave();
|
||||||
location,
|
|
||||||
locationLabel,
|
|
||||||
description,
|
|
||||||
skipAddLabelButton: props.skipAddLabelButton,
|
|
||||||
editDescription: props.editDescription,
|
|
||||||
editLocation: props.editLocation,
|
|
||||||
confirmationState: false,
|
|
||||||
displayComponent: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
this.mainElement = React.createRef();
|
|
||||||
this.descriptionInput = React.createRef();
|
|
||||||
this.initialItems = OHIFLabellingData;
|
|
||||||
this.currentItems = cloneDeep(this.initialItems);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate = () => {
|
|
||||||
if (this.state.editDescription) {
|
|
||||||
this.descriptionInput.current.focus();
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
const descriptionSave = () => {
|
||||||
let mainElementClassName = 'labellingComponent';
|
const description = descriptionInput.current.value;
|
||||||
if (this.state.editDescription) {
|
updateLabelling({ description });
|
||||||
mainElementClassName += ' editDescription';
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
setState(state => ({
|
||||||
<LabellingTransition
|
...state,
|
||||||
displayComponent={this.state.displayComponent}
|
|
||||||
onTransitionExit={this.props.labellingDoneCallback}
|
|
||||||
>
|
|
||||||
<>
|
|
||||||
<div
|
|
||||||
className={mainElementClassName}
|
|
||||||
ref={this.mainElement}
|
|
||||||
onMouseLeave={this.fadeOutAndLeave}
|
|
||||||
onMouseEnter={this.clearFadeOutTimer}
|
|
||||||
>
|
|
||||||
{this.labellingStateFragment()}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
</LabellingTransition>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
labellingStateFragment = () => {
|
|
||||||
const {
|
|
||||||
skipAddLabelButton,
|
|
||||||
editLocation,
|
|
||||||
description,
|
description,
|
||||||
locationLabel,
|
editDescription: false,
|
||||||
} = this.state;
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectTreeSelectCallback = (event, itemSelected) => {
|
||||||
|
const location = itemSelected.value;
|
||||||
|
const locationLabel = itemSelected.label;
|
||||||
|
updateLabelling({ location });
|
||||||
|
|
||||||
|
setState(state => ({
|
||||||
|
...state,
|
||||||
|
editLocation: false,
|
||||||
|
measurementData: {
|
||||||
|
...state.measurementData,
|
||||||
|
location,
|
||||||
|
locationLabel,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const showLabelling = () => {
|
||||||
|
setState(state => ({
|
||||||
|
...state,
|
||||||
|
skipAddLabelButton: true,
|
||||||
|
editLocation: false,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Waits for 1 sec to dismiss the labelling component.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
const fadeOutAndLeave = () =>
|
||||||
|
setFadeOutTimer(setTimeout(fadeOutAndLeaveFast, 1000));
|
||||||
|
|
||||||
|
const fadeOutAndLeaveFast = () => setShowComponent(false);
|
||||||
|
|
||||||
|
const clearFadeOutTimer = () => {
|
||||||
|
if (fadeOutTimer) {
|
||||||
|
clearTimeout(fadeOutTimer);
|
||||||
|
setFadeOutTimer(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const descriptionDialogUpdate = description => {
|
||||||
|
updateLabelling({ description });
|
||||||
|
labellingDoneCallback();
|
||||||
|
};
|
||||||
|
|
||||||
|
const labellingStateFragment = () => {
|
||||||
|
const { skipAddLabelButton, editLocation, measurementData } = state;
|
||||||
|
const { description, locationLabel, location } = measurementData;
|
||||||
|
|
||||||
if (!skipAddLabelButton) {
|
if (!skipAddLabelButton) {
|
||||||
return (
|
return (
|
||||||
<>
|
<button
|
||||||
<button
|
type="button"
|
||||||
type="button"
|
className="addLabelButton"
|
||||||
className="addLabelButton"
|
onClick={showLabelling}
|
||||||
onClick={this.showLabelling}
|
>
|
||||||
>
|
{location ? 'Edit' : 'Add'} Label
|
||||||
{this.state.location ? 'Edit' : 'Add'} Label
|
</button>
|
||||||
</button>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
if (editLocation) {
|
if (editLocation) {
|
||||||
return (
|
return (
|
||||||
<SelectTree
|
<SelectTree
|
||||||
items={this.currentItems}
|
items={OHIFLabellingData}
|
||||||
columns={1}
|
columns={1}
|
||||||
onSelected={this.selectTreeSelectCallback}
|
onSelected={selectTreeSelectCallback}
|
||||||
selectTreeFirstTitle="Assign Label"
|
selectTreeFirstTitle="Assign Label"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div
|
<div className="checkIconWrapper" onClick={fadeOutAndLeaveFast}>
|
||||||
className="checkIconWrapper"
|
|
||||||
onClick={this.fadeOutAndLeaveFast}
|
|
||||||
>
|
|
||||||
<Icon name="check" className="checkIcon" />
|
<Icon name="check" className="checkIcon" />
|
||||||
</div>
|
</div>
|
||||||
<div className="locationDescriptionWrapper">
|
<div className="locationDescriptionWrapper">
|
||||||
@@ -114,10 +167,10 @@ export default class LabellingFlow extends Component {
|
|||||||
<div className="description">
|
<div className="description">
|
||||||
<input
|
<input
|
||||||
id="descriptionInput"
|
id="descriptionInput"
|
||||||
ref={this.descriptionInput}
|
ref={descriptionInput}
|
||||||
defaultValue={description || ''}
|
defaultValue={description || ''}
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
onKeyPress={this.handleKeyPress}
|
onKeyPress={handleKeyPress}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -125,14 +178,14 @@ export default class LabellingFlow extends Component {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="commonButton left"
|
className="commonButton left"
|
||||||
onClick={this.relabel}
|
onClick={relabel}
|
||||||
>
|
>
|
||||||
Relabel
|
Relabel
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="commonButton right"
|
className="commonButton right"
|
||||||
onClick={this.setDescriptionUpdateMode}
|
onClick={setDescriptionUpdateMode}
|
||||||
>
|
>
|
||||||
{description ? 'Edit ' : 'Add '}
|
{description ? 'Edit ' : 'Add '}
|
||||||
Description
|
Description
|
||||||
@@ -142,14 +195,14 @@ export default class LabellingFlow extends Component {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="commonButton left"
|
className="commonButton left"
|
||||||
onClick={this.descriptionCancel}
|
onClick={descriptionCancel}
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="commonButton right"
|
className="commonButton right"
|
||||||
onClick={this.descriptionSave}
|
onClick={descriptionSave}
|
||||||
>
|
>
|
||||||
Save
|
Save
|
||||||
</button>
|
</button>
|
||||||
@@ -160,77 +213,51 @@ export default class LabellingFlow extends Component {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
relabel = event => this.setState({ editLocation: true });
|
if (editDescriptionOnDialog) {
|
||||||
|
return (
|
||||||
setDescriptionUpdateMode = () => {
|
<EditDescriptionDialog
|
||||||
this.descriptionInput.current.focus();
|
onCancel={labellingDoneCallback}
|
||||||
this.setState({ editDescription: true });
|
onUpdate={descriptionDialogUpdate}
|
||||||
};
|
measurementData={state.measurementData}
|
||||||
|
/>
|
||||||
descriptionCancel = () => {
|
|
||||||
const { description = '' } = cloneDeep(this.state);
|
|
||||||
this.descriptionInput.current.value = description;
|
|
||||||
this.setState({ editDescription: false });
|
|
||||||
};
|
|
||||||
|
|
||||||
handleKeyPress = e => {
|
|
||||||
if (e.key === 'Enter') {
|
|
||||||
this.descriptionSave();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
descriptionSave = () => {
|
|
||||||
const description = this.descriptionInput.current.value;
|
|
||||||
this.props.updateLabelling({ description });
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
description,
|
|
||||||
editDescription: false,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
selectTreeSelectCallback = (event, itemSelected) => {
|
|
||||||
const location = itemSelected.value;
|
|
||||||
this.props.updateLabelling({ location });
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
editLocation: false,
|
|
||||||
confirmationState: true,
|
|
||||||
location: itemSelected.value,
|
|
||||||
locationLabel: itemSelected.label,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (this.isTouchScreen) {
|
|
||||||
this.setTimeout = setTimeout(() => {
|
|
||||||
this.setState({
|
|
||||||
displayComponent: false,
|
|
||||||
});
|
|
||||||
}, 2000);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
showLabelling = () => {
|
|
||||||
this.setState({
|
|
||||||
skipAddLabelButton: true,
|
|
||||||
editLocation: false,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
fadeOutAndLeave = () => {
|
|
||||||
// Wait for 1 sec to dismiss the labelling component
|
|
||||||
this.fadeOutTimer = setTimeout(
|
|
||||||
() => this.setState({ displayComponent: false }),
|
|
||||||
1000
|
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
fadeOutAndLeaveFast = () => this.setState({ displayComponent: false });
|
return (
|
||||||
|
<LabellingTransition
|
||||||
|
displayComponent={showComponent}
|
||||||
|
onTransitionExit={labellingDoneCallback}
|
||||||
|
>
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
className={`labellingComponent ${state.editDescription &&
|
||||||
|
'editDescription'}`}
|
||||||
|
onMouseLeave={fadeOutAndLeave}
|
||||||
|
onMouseEnter={clearFadeOutTimer}
|
||||||
|
>
|
||||||
|
{labellingStateFragment()}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
</LabellingTransition>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
clearFadeOutTimer = () => {
|
LabellingFlow.propTypes = {
|
||||||
if (!this.fadeOutTimer) {
|
measurementData: PropTypes.object.isRequired,
|
||||||
return;
|
labellingDoneCallback: PropTypes.func.isRequired,
|
||||||
}
|
updateLabelling: PropTypes.func.isRequired,
|
||||||
|
initialTopDistance: PropTypes.number,
|
||||||
|
skipAddLabelButton: PropTypes.bool,
|
||||||
|
editLocation: PropTypes.bool,
|
||||||
|
editDescription: PropTypes.bool,
|
||||||
|
editDescriptionOnDialog: PropTypes.bool,
|
||||||
|
};
|
||||||
|
|
||||||
clearTimeout(this.fadeOutTimer);
|
LabellingFlow.defaultProps = {
|
||||||
};
|
skipAddLabelButton: false,
|
||||||
}
|
editLocation: false,
|
||||||
|
editDescription: false,
|
||||||
|
editDescriptionOnDialog: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LabellingFlow;
|
||||||
@@ -1,103 +0,0 @@
|
|||||||
import React, { Component } from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import cloneDeep from 'lodash.clonedeep';
|
|
||||||
|
|
||||||
import EditDescriptionDialog from './../EditDescriptionDialog/EditDescriptionDialog.js';
|
|
||||||
import LabellingFlow from './LabellingFlow.js';
|
|
||||||
import './LabellingManager.css';
|
|
||||||
|
|
||||||
export default class LabellingManager extends Component {
|
|
||||||
static propTypes = {
|
|
||||||
measurementData: PropTypes.object.isRequired,
|
|
||||||
labellingDoneCallback: PropTypes.func.isRequired,
|
|
||||||
updateLabelling: PropTypes.func.isRequired,
|
|
||||||
skipAddLabelButton: PropTypes.bool,
|
|
||||||
editLocation: PropTypes.bool,
|
|
||||||
editDescription: PropTypes.bool,
|
|
||||||
editDescriptionOnDialog: PropTypes.bool,
|
|
||||||
};
|
|
||||||
|
|
||||||
static defaultProps = {
|
|
||||||
skipAddLabelButton: false,
|
|
||||||
editLocation: false,
|
|
||||||
editDescription: false,
|
|
||||||
editDescriptionOnDialog: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
|
|
||||||
const measurementData = cloneDeep(props.measurementData);
|
|
||||||
this.treatMeasurementData(measurementData);
|
|
||||||
|
|
||||||
let editLocation = props.editLocation;
|
|
||||||
if (!props.editDescription && !props.editLocation) {
|
|
||||||
editLocation = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
skipAddLabelButton: props.skipAddLabelButton,
|
|
||||||
editLocation: editLocation,
|
|
||||||
editDescription: props.editDescription,
|
|
||||||
editDescriptionOnDialog: props.editDescriptionOnDialog,
|
|
||||||
measurementData: measurementData,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount = () => {
|
|
||||||
document.addEventListener('touchstart', this.onTouchStart);
|
|
||||||
};
|
|
||||||
|
|
||||||
componentWillUnmount = () => {
|
|
||||||
document.removeEventListener('touchstart', this.onTouchStart);
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return this.getRenderComponent();
|
|
||||||
}
|
|
||||||
|
|
||||||
getRenderComponent = () => {
|
|
||||||
const {
|
|
||||||
editLocation,
|
|
||||||
editDescription,
|
|
||||||
editDescriptionOnDialog,
|
|
||||||
measurementData,
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
if (editDescriptionOnDialog) {
|
|
||||||
return (
|
|
||||||
<EditDescriptionDialog
|
|
||||||
onCancel={this.props.labellingDoneCallback}
|
|
||||||
onUpdate={this.descriptionDialogUpdate}
|
|
||||||
measurementData={measurementData}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (editLocation || editDescription) {
|
|
||||||
return <LabellingFlow {...this.props} />;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
treatMeasurementData = measurementData => {
|
|
||||||
const { editDescription, editLocation } = this.props;
|
|
||||||
|
|
||||||
if (editDescription) {
|
|
||||||
measurementData.description = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (editLocation) {
|
|
||||||
measurementData.location = undefined;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
responseDialogUpdate = response => {
|
|
||||||
this.props.updateLabelling({ response });
|
|
||||||
this.props.labellingDoneCallback();
|
|
||||||
};
|
|
||||||
|
|
||||||
descriptionDialogUpdate = description => {
|
|
||||||
this.props.updateLabelling({ description });
|
|
||||||
this.props.labellingDoneCallback();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,13 +1,20 @@
|
|||||||
import { connect } from "react-redux";
|
import { connect } from 'react-redux';
|
||||||
import Viewer from "./Viewer.js";
|
import Viewer from './Viewer.js';
|
||||||
import OHIF from "@ohif/core";
|
import OHIF from '@ohif/core';
|
||||||
|
|
||||||
const { setTimepoints, setMeasurements } = OHIF.redux.actions;
|
const { setTimepoints, setMeasurements } = OHIF.redux.actions;
|
||||||
|
|
||||||
const mapStateToProps = (state, ownProps) => {
|
const getActiveServer = servers => {
|
||||||
|
const isActive = a => a.active === true;
|
||||||
|
return servers.servers.find(isActive);
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapStateToProps = state => {
|
||||||
|
const { viewports, servers } = state;
|
||||||
return {
|
return {
|
||||||
viewports: state.viewports.viewportSpecificData,
|
viewports: viewports.viewportSpecificData,
|
||||||
activeViewportIndex: state.viewports.activeViewportIndex
|
activeViewportIndex: viewports.activeViewportIndex,
|
||||||
|
activeServer: getActiveServer(servers),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -18,7 +25,7 @@ const mapDispatchToProps = dispatch => {
|
|||||||
},
|
},
|
||||||
onMeasurementsUpdated: measurements => {
|
onMeasurementsUpdated: measurements => {
|
||||||
dispatch(setMeasurements(measurements));
|
dispatch(setMeasurements(measurements));
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import ConnectedStudyBrowser from './ConnectedStudyBrowser.js';
|
|||||||
import ConnectedViewerMain from './ConnectedViewerMain.js';
|
import ConnectedViewerMain from './ConnectedViewerMain.js';
|
||||||
import SidePanel from './../components/SidePanel.js';
|
import SidePanel from './../components/SidePanel.js';
|
||||||
import { extensionManager } from './../App.js';
|
import { extensionManager } from './../App.js';
|
||||||
|
import DICOMSR from '../lib/DICOMSR';
|
||||||
|
|
||||||
// Contexts
|
// Contexts
|
||||||
import WhiteLabellingContext from '../context/WhiteLabellingContext.js';
|
import WhiteLabellingContext from '../context/WhiteLabellingContext.js';
|
||||||
@@ -57,21 +58,31 @@ class Viewer extends Component {
|
|||||||
static propTypes = {
|
static propTypes = {
|
||||||
studies: PropTypes.array,
|
studies: PropTypes.array,
|
||||||
studyInstanceUids: PropTypes.array,
|
studyInstanceUids: PropTypes.array,
|
||||||
|
activeServer: PropTypes.shape({
|
||||||
|
type: PropTypes.string,
|
||||||
|
wadoRoot: PropTypes.string,
|
||||||
|
}),
|
||||||
onTimepointsUpdated: PropTypes.func,
|
onTimepointsUpdated: PropTypes.func,
|
||||||
onMeasurementsUpdated: PropTypes.func,
|
onMeasurementsUpdated: PropTypes.func,
|
||||||
// window.store.getState().viewports.viewportSpecificData
|
// window.store.getState().viewports.viewportSpecificData
|
||||||
viewports: PropTypes.object.isRequired,
|
viewports: PropTypes.object.isRequired,
|
||||||
// window.store.getState().viewports.activeViewportIndex
|
// window.store.getState().viewports.activeViewportIndex
|
||||||
activeViewportIndex: PropTypes.number.isRequired,
|
activeViewportIndex: PropTypes.number.isRequired,
|
||||||
|
isStudyLoaded: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
|
const { activeServer } = this.props;
|
||||||
|
const server = Object.assign({}, activeServer);
|
||||||
|
|
||||||
OHIF.measurements.MeasurementApi.setConfiguration({
|
OHIF.measurements.MeasurementApi.setConfiguration({
|
||||||
dataExchange: {
|
dataExchange: {
|
||||||
retrieve: this.retrieveMeasurements,
|
retrieve: DICOMSR.retrieveMeasurements,
|
||||||
store: this.storeMeasurements,
|
store: DICOMSR.storeMeasurements,
|
||||||
},
|
},
|
||||||
|
server,
|
||||||
});
|
});
|
||||||
|
|
||||||
OHIF.measurements.TimepointApi.setConfiguration({
|
OHIF.measurements.TimepointApi.setConfiguration({
|
||||||
@@ -93,18 +104,6 @@ class Viewer extends Component {
|
|||||||
thumbnails: [],
|
thumbnails: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
retrieveMeasurements = (patientId, timepointIds) => {
|
|
||||||
OHIF.log.info('retrieveMeasurements');
|
|
||||||
// TODO: Retrieve the measurements from the latest available SR
|
|
||||||
return Promise.resolve();
|
|
||||||
};
|
|
||||||
|
|
||||||
storeMeasurements = (measurementData, timepointIds) => {
|
|
||||||
OHIF.log.info('storeMeasurements');
|
|
||||||
// TODO: Store the measurements into a new SR sent to the active server
|
|
||||||
return Promise.resolve();
|
|
||||||
};
|
|
||||||
|
|
||||||
retrieveTimepoints = filter => {
|
retrieveTimepoints = filter => {
|
||||||
OHIF.log.info('retrieveTimepoints');
|
OHIF.log.info('retrieveTimepoints');
|
||||||
|
|
||||||
@@ -171,7 +170,7 @@ class Viewer extends Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
const { studies } = this.props;
|
const { studies, isStudyLoaded } = this.props;
|
||||||
const { TimepointApi, MeasurementApi } = OHIF.measurements;
|
const { TimepointApi, MeasurementApi } = OHIF.measurements;
|
||||||
const currentTimepointId = 'TimepointId';
|
const currentTimepointId = 'TimepointId';
|
||||||
|
|
||||||
@@ -191,8 +190,11 @@ class Viewer extends Component {
|
|||||||
const patientId = studies[0] && studies[0].patientId;
|
const patientId = studies[0] && studies[0].patientId;
|
||||||
|
|
||||||
timepointApi.retrieveTimepoints({ patientId });
|
timepointApi.retrieveTimepoints({ patientId });
|
||||||
measurementApi.retrieveMeasurements(patientId, [currentTimepointId]);
|
if (isStudyLoaded) {
|
||||||
|
this.measurementApi.retrieveMeasurements(patientId, [
|
||||||
|
currentTimepointId,
|
||||||
|
]);
|
||||||
|
}
|
||||||
this.setState({
|
this.setState({
|
||||||
thumbnails: _mapStudiesToThumbnails(studies),
|
thumbnails: _mapStudiesToThumbnails(studies),
|
||||||
});
|
});
|
||||||
@@ -200,18 +202,19 @@ class Viewer extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
componentDidUpdate(prevProps) {
|
||||||
if (this.props.studies !== prevProps.studies) {
|
const { studies, isStudyLoaded } = this.props;
|
||||||
const { studies } = this.props;
|
if (studies !== prevProps.studies) {
|
||||||
const patientId = studies[0] && studies[0].patientId;
|
|
||||||
const currentTimepointId = this.currentTimepointId;
|
|
||||||
|
|
||||||
this.timepointApi.retrieveTimepoints({ patientId });
|
|
||||||
this.measurementApi.retrieveMeasurements(patientId, [currentTimepointId]);
|
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
thumbnails: _mapStudiesToThumbnails(studies),
|
thumbnails: _mapStudiesToThumbnails(studies),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (isStudyLoaded && isStudyLoaded !== prevProps.isStudyLoaded) {
|
||||||
|
const patientId = studies[0] && studies[0].patientId;
|
||||||
|
const { currentTimepointId } = this;
|
||||||
|
|
||||||
|
this.timepointApi.retrieveTimepoints({ patientId });
|
||||||
|
this.measurementApi.retrieveMeasurements(patientId, [currentTimepointId]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
@@ -294,11 +297,11 @@ class Viewer extends Component {
|
|||||||
activeIndex={this.props.activeViewportIndex}
|
activeIndex={this.props.activeViewportIndex}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<ConnectedStudyBrowser
|
<ConnectedStudyBrowser
|
||||||
studies={this.state.thumbnails}
|
studies={this.state.thumbnails}
|
||||||
studyMetadata={this.props.studies}
|
studyMetadata={this.props.studies}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</SidePanel>
|
</SidePanel>
|
||||||
|
|
||||||
{/* MAIN */}
|
{/* MAIN */}
|
||||||
@@ -334,7 +337,7 @@ export default Viewer;
|
|||||||
* @param {Study[]} studies
|
* @param {Study[]} studies
|
||||||
* @param {DisplaySet[]} studies[].displaySets
|
* @param {DisplaySet[]} studies[].displaySets
|
||||||
*/
|
*/
|
||||||
const _mapStudiesToThumbnails = function(studies) {
|
const _mapStudiesToThumbnails = function (studies) {
|
||||||
return studies.map(study => {
|
return studies.map(study => {
|
||||||
const { studyInstanceUid } = study;
|
const { studyInstanceUid } = study;
|
||||||
|
|
||||||
|
|||||||
@@ -178,6 +178,7 @@ function ViewerRetrieveStudyData({
|
|||||||
// hooks
|
// hooks
|
||||||
const [error, setError] = useState(false);
|
const [error, setError] = useState(false);
|
||||||
const [studies, setStudies] = useState([]);
|
const [studies, setStudies] = useState([]);
|
||||||
|
const [isStudyLoaded, setIsStudyLoaded] = useState(false);
|
||||||
const snackbarContext = useSnackbarContext();
|
const snackbarContext = useSnackbarContext();
|
||||||
const { appConfig = {} } = useContext(AppContext);
|
const { appConfig = {} } = useContext(AppContext);
|
||||||
const { filterQueryParam: isFilterStrategy = false } = appConfig;
|
const { filterQueryParam: isFilterStrategy = false } = appConfig;
|
||||||
@@ -218,6 +219,7 @@ function ViewerRetrieveStudyData({
|
|||||||
);
|
);
|
||||||
|
|
||||||
setStudies([...studies, study]);
|
setStudies([...studies, study]);
|
||||||
|
setIsStudyLoaded(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -332,7 +334,11 @@ function ViewerRetrieveStudyData({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ConnectedViewer studies={studies} studyInstanceUids={studyInstanceUids} />
|
<ConnectedViewer
|
||||||
|
studies={studies}
|
||||||
|
isStudyLoaded={isStudyLoaded}
|
||||||
|
studyInstanceUids={studyInstanceUids}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import { log, studies, utils } from '@ohif/core';
|
||||||
|
import { retrieveMeasurementFromSR, stowSRFromMeasurements } from './handleStructuredReport';
|
||||||
|
import findMostRecentStructuredReport from './utils/findMostRecentStructuredReport';
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @typedef serverType
|
||||||
|
* @property {string} type - type of the server
|
||||||
|
* @property {string} wadoRoot - server wado root url
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function to be registered into MeasurementAPI to retrieve measurements from DICOM Structured Reports
|
||||||
|
*
|
||||||
|
* @param {serverType} server
|
||||||
|
* @returns {Promise} Should resolve with OHIF measurementData object
|
||||||
|
*/
|
||||||
|
const retrieveMeasurements = server => {
|
||||||
|
log.info('[DICOMSR] retrieveMeasurements');
|
||||||
|
|
||||||
|
if (!server || server.type !== 'dicomWeb') {
|
||||||
|
log.error('[DICOMSR] DicomWeb server is required!');
|
||||||
|
return Promise.reject({});
|
||||||
|
}
|
||||||
|
|
||||||
|
const serverUrl = server.wadoRoot;
|
||||||
|
const studies = utils.studyMetadataManager.all();
|
||||||
|
const latestSeries = findMostRecentStructuredReport(studies);
|
||||||
|
|
||||||
|
if (!latestSeries) return Promise.resolve({});
|
||||||
|
|
||||||
|
return retrieveMeasurementFromSR(latestSeries, studies, serverUrl);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function to be registered into MeasurementAPI to store measurements into DICOM Structured Reports
|
||||||
|
*
|
||||||
|
* @param {Object} measurementData - OHIF measurementData object
|
||||||
|
* @param {Object} filter
|
||||||
|
* @param {serverType} server
|
||||||
|
* @returns {Object} With message to be displayed on success
|
||||||
|
*/
|
||||||
|
const storeMeasurements = async (measurementData, filter, server) => {
|
||||||
|
log.info('[DICOMSR] storeMeasurements');
|
||||||
|
|
||||||
|
if (!server || server.type !== 'dicomWeb') {
|
||||||
|
log.error('[DICOMSR] DicomWeb server is required!');
|
||||||
|
return Promise.reject({});
|
||||||
|
}
|
||||||
|
|
||||||
|
const serverUrl = server.wadoRoot;
|
||||||
|
const firstMeasurementKey = Object.keys(measurementData)[0];
|
||||||
|
const firstMeasurement = measurementData[firstMeasurementKey][0];
|
||||||
|
const studyInstanceUid =
|
||||||
|
firstMeasurement && firstMeasurement.studyInstanceUid;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await stowSRFromMeasurements(measurementData, serverUrl);
|
||||||
|
if (studyInstanceUid) {
|
||||||
|
studies.deleteStudyMetadataPromise(studyInstanceUid);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: 'Measurements were saved with success',
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
log.error(`[DICOMSR] Error while saving the measurements: ${error.message}`);
|
||||||
|
throw new Error('Error while saving the measurements.');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export { retrieveMeasurements, storeMeasurements };
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
import * as dcmjs from 'dcmjs';
|
||||||
|
import { DICOMWeb } from '@ohif/core';
|
||||||
|
import parseDicomStructuredReport from './parseDicomStructuredReport';
|
||||||
|
import parseMeasurementsData from './parseMeasurementsData';
|
||||||
|
import getAllDisplaySets from './utils/getAllDisplaySets';
|
||||||
|
|
||||||
|
import { api } from 'dicomweb-client';
|
||||||
|
|
||||||
|
const VERSION_NAME = 'dcmjs-0.0';
|
||||||
|
const TRANSFER_SYNTAX_UID = '1.2.840.10008.1.2.1';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function to retrieve measurements from DICOM Structured Reports coming from determined server
|
||||||
|
*
|
||||||
|
* @param {Array} series - List of all series metaData loaded
|
||||||
|
* @param {Array} studies - List of all studies metaData loaded
|
||||||
|
* @param {string} serverUrl - Server URL to be used on request
|
||||||
|
* @returns {Object} MeasurementData
|
||||||
|
*/
|
||||||
|
const retrieveMeasurementFromSR = async (series, studies, serverUrl) => {
|
||||||
|
const config = {
|
||||||
|
url: serverUrl,
|
||||||
|
headers: DICOMWeb.getAuthorizationHeader(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const dicomWeb = new api.DICOMwebClient(config);
|
||||||
|
|
||||||
|
const instance = series.getFirstInstance();
|
||||||
|
const options = {
|
||||||
|
studyInstanceUID: instance.getStudyInstanceUID(),
|
||||||
|
seriesInstanceUID: instance.getSeriesInstanceUID(),
|
||||||
|
sopInstanceUID: instance.getSOPInstanceUID(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const part10SRArrayBuffer = await dicomWeb.retrieveInstance(options);
|
||||||
|
const displaySets = getAllDisplaySets(studies);
|
||||||
|
const measurementsData = parseDicomStructuredReport(part10SRArrayBuffer, displaySets);
|
||||||
|
|
||||||
|
return measurementsData;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function to store measurements to DICOM Structured Reports in determined server
|
||||||
|
*
|
||||||
|
* @param {Object} measurements - OHIF measurementData object
|
||||||
|
* @param {string} serverUrl - Server URL to be used on request
|
||||||
|
* @returns {Promise}
|
||||||
|
*/
|
||||||
|
const stowSRFromMeasurements = async (measurements, serverUrl) => {
|
||||||
|
const { dataset } = parseMeasurementsData(
|
||||||
|
measurements
|
||||||
|
);
|
||||||
|
const { DicomMetaDictionary, DicomDict } = dcmjs.data;
|
||||||
|
const meta = {
|
||||||
|
FileMetaInformationVersion:
|
||||||
|
dataset._meta.FileMetaInformationVersion.Value,
|
||||||
|
MediaStorageSOPClassUID: dataset.SOPClassUID,
|
||||||
|
MediaStorageSOPInstanceUID: dataset.SOPInstanceUID,
|
||||||
|
TransferSyntaxUID: TRANSFER_SYNTAX_UID,
|
||||||
|
ImplementationClassUID: DicomMetaDictionary.uid(),
|
||||||
|
ImplementationVersionName: VERSION_NAME,
|
||||||
|
};
|
||||||
|
|
||||||
|
const denaturalized = DicomMetaDictionary.denaturalizeDataset(meta);
|
||||||
|
const dicomDict = new DicomDict(denaturalized);
|
||||||
|
|
||||||
|
dicomDict.dict = DicomMetaDictionary.denaturalizeDataset(dataset);
|
||||||
|
|
||||||
|
const part10Buffer = dicomDict.write();
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
url: serverUrl,
|
||||||
|
headers: DICOMWeb.getAuthorizationHeader(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const dicomWeb = new api.DICOMwebClient(config);
|
||||||
|
const options = {
|
||||||
|
datasets: [part10Buffer],
|
||||||
|
};
|
||||||
|
|
||||||
|
await dicomWeb.storeInstances(options);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { retrieveMeasurementFromSR, stowSRFromMeasurements };
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { retrieveMeasurements, storeMeasurements } from './dataExchange';
|
||||||
|
import isToolSupported from './utils/isToolSupported';
|
||||||
|
|
||||||
|
const DICOMSR = {
|
||||||
|
retrieveMeasurements,
|
||||||
|
storeMeasurements,
|
||||||
|
isToolSupported,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DICOMSR;
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
import * as dcmjs from 'dcmjs';
|
||||||
|
import findInstanceMetadataBySopInstanceUid from './utils/findInstanceMetadataBySopInstanceUid';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function to parse the part10 array buffer that comes from a DICOM Structured report into measurementData
|
||||||
|
* measurementData format is a viewer specific format to be stored into the redux and consumed by other components
|
||||||
|
* (e.g. measurement table)
|
||||||
|
*
|
||||||
|
* @param {ArrayBuffer} part10SRArrayBuffer
|
||||||
|
* @param {Array} displaySets
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
const parseDicomStructuredReport = (part10SRArrayBuffer, displaySets) => {
|
||||||
|
// Get the dicom data as an Object
|
||||||
|
const dicomData = dcmjs.data.DicomMessage.readFile(part10SRArrayBuffer);
|
||||||
|
const dataset = dcmjs.data.DicomMetaDictionary.naturalizeDataset(
|
||||||
|
dicomData.dict
|
||||||
|
);
|
||||||
|
|
||||||
|
const { MeasurementReport } = dcmjs.adapters.Cornerstone;
|
||||||
|
const storedMeasurementByToolType = MeasurementReport.generateToolState(
|
||||||
|
dataset
|
||||||
|
);
|
||||||
|
const measurementData = {};
|
||||||
|
let measurementNumber = 0;
|
||||||
|
|
||||||
|
Object.keys(storedMeasurementByToolType).forEach(toolName => {
|
||||||
|
const measurements = storedMeasurementByToolType[toolName];
|
||||||
|
measurementData[toolName] = [];
|
||||||
|
|
||||||
|
measurements.forEach(measurement => {
|
||||||
|
const instanceMetadata = findInstanceMetadataBySopInstanceUid(
|
||||||
|
displaySets,
|
||||||
|
measurement.sopInstanceUid
|
||||||
|
);
|
||||||
|
const { _study: study, _series: series } = instanceMetadata;
|
||||||
|
const { studyInstanceUid, patientId } = study;
|
||||||
|
const { seriesInstanceUid } = series;
|
||||||
|
const { sopInstanceUid, frameIndex } = measurement;
|
||||||
|
const imagePath = getImagePath(studyInstanceUid, seriesInstanceUid, sopInstanceUid, frameIndex);
|
||||||
|
|
||||||
|
const imageId = instanceMetadata.getImageId();
|
||||||
|
if (!imageId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: We need the currentTimepointID set into the viewer
|
||||||
|
const currentTimepointId = 'TimepointId';
|
||||||
|
|
||||||
|
const toolData = Object.assign({}, measurement, {
|
||||||
|
imageId,
|
||||||
|
imagePath,
|
||||||
|
seriesInstanceUid,
|
||||||
|
studyInstanceUid,
|
||||||
|
patientId,
|
||||||
|
measurementNumber: ++measurementNumber,
|
||||||
|
timepointId: currentTimepointId,
|
||||||
|
toolType: toolName,
|
||||||
|
_id: imageId + measurementNumber,
|
||||||
|
});
|
||||||
|
|
||||||
|
measurementData[toolName].push(toolData);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return measurementData;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function to create imagePath with all imageData related
|
||||||
|
*
|
||||||
|
* @param {string} studyInstanceUid
|
||||||
|
* @param {string} seriesInstanceUid
|
||||||
|
* @param {string} sopInstanceUid
|
||||||
|
* @param {string} frameIndex
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
const getImagePath = (studyInstanceUid, seriesInstanceUid, sopInstanceUid, frameIndex) => {
|
||||||
|
return [studyInstanceUid, seriesInstanceUid, sopInstanceUid, frameIndex].join('_');
|
||||||
|
}
|
||||||
|
|
||||||
|
export default parseDicomStructuredReport;
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import * as dcmjs from 'dcmjs';
|
||||||
|
import { log, measurements } from '@ohif/core';
|
||||||
|
import cornerstone from 'cornerstone-core';
|
||||||
|
import isToolSupported from './utils/isToolSupported';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function to parse OHIF viewer measurementData into a dcmjs MeasurementReport
|
||||||
|
*
|
||||||
|
* @param {Object} measurementsData - OHIF measurementData object
|
||||||
|
* @returns {Object} Dataset: measurement report from dcmjs
|
||||||
|
*/
|
||||||
|
const parseMeasurementsData = measurementsData => {
|
||||||
|
const { MeasurementReport } = dcmjs.adapters.Cornerstone;
|
||||||
|
const { getImageIdForImagePath } = measurements;
|
||||||
|
|
||||||
|
const toolState = {};
|
||||||
|
const unsupportedTools = [];
|
||||||
|
|
||||||
|
Object.keys(measurementsData).forEach(measurementType => {
|
||||||
|
const annotations = measurementsData[measurementType];
|
||||||
|
|
||||||
|
annotations.forEach(annotation => {
|
||||||
|
const { toolType, imagePath } = annotation;
|
||||||
|
|
||||||
|
if (isToolSupported(toolType)) {
|
||||||
|
const imageId = getImageIdForImagePath(imagePath);
|
||||||
|
toolState[imageId] = toolState[imageId] || {};
|
||||||
|
toolState[imageId][toolType] = toolState[imageId][toolType] || {
|
||||||
|
data: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
toolState[imageId][toolType].data.push(annotation);
|
||||||
|
} else {
|
||||||
|
unsupportedTools.push(toolType);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (unsupportedTools.length > 0) {
|
||||||
|
log.warn(
|
||||||
|
`[DICOMSR] Tooltypes not supported: ${unsupportedTools.join(', ')}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const report = MeasurementReport.generateReport(
|
||||||
|
toolState,
|
||||||
|
cornerstone.metaData
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
dataset: report.dataset,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default parseMeasurementsData;
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
/**
|
||||||
|
* Should Find the requested instance metadata into the displaySets and return
|
||||||
|
*
|
||||||
|
* @param {Array} displaySets - List of displaySets
|
||||||
|
* @param {string} sopInstanceUid - sopInstanceUID to look for
|
||||||
|
* @returns {Object} - instance metadata found
|
||||||
|
*/
|
||||||
|
const findInstanceMetadataBySopInstanceUid = (displaySets, sopInstanceUid) => {
|
||||||
|
let instanceFound;
|
||||||
|
|
||||||
|
displaySets.find(displaySet => {
|
||||||
|
if (!displaySet.images) return false;
|
||||||
|
|
||||||
|
instanceFound = displaySet.images.find(instanceMetadata => instanceMetadata._sopInstanceUID === sopInstanceUid);
|
||||||
|
|
||||||
|
return !!instanceFound;
|
||||||
|
});
|
||||||
|
|
||||||
|
return instanceFound;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default findInstanceMetadataBySopInstanceUid;
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
/**
|
||||||
|
* Should find the most recent Structured Report metadata
|
||||||
|
*
|
||||||
|
* @param {Array} studies
|
||||||
|
* @returns {Object} Series
|
||||||
|
*/
|
||||||
|
const findMostRecentStructuredReport = (studies) => {
|
||||||
|
let mostRecentStructuredReport;
|
||||||
|
|
||||||
|
studies.forEach(study => {
|
||||||
|
const allSeries = study.getSeries ? study.getSeries() : [];
|
||||||
|
allSeries.forEach(series => {
|
||||||
|
|
||||||
|
if (isStructuredReportSeries(series)) {
|
||||||
|
if (!mostRecentStructuredReport || compareSeriesDate(series, mostRecentStructuredReport)) {
|
||||||
|
mostRecentStructuredReport = series;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return mostRecentStructuredReport;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if series sopClassUID matches with the supported Structured Reports sopClassUID
|
||||||
|
*
|
||||||
|
* @param {Object} series - Series metadata
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
const isStructuredReportSeries = (series) => {
|
||||||
|
const supportedSopClassUIDs = [
|
||||||
|
'1.2.840.10008.5.1.4.1.1.88.22',
|
||||||
|
'1.2.840.10008.5.1.4.1.1.11.1',
|
||||||
|
];
|
||||||
|
|
||||||
|
const firstInstance = series.getFirstInstance();
|
||||||
|
const sopClassUid = firstInstance._instance.sopClassUid;
|
||||||
|
|
||||||
|
return supportedSopClassUIDs.includes(sopClassUid);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checkes if series1 is newer than series2
|
||||||
|
*
|
||||||
|
* @param {Object} series1 - Series Metadata 1
|
||||||
|
* @param {Object} series2 - Series Metadata 2
|
||||||
|
* @returns {boolean} true/false if series1 is newer than series2
|
||||||
|
*/
|
||||||
|
const compareSeriesDate = (series1, series2) => {
|
||||||
|
return series1._data.seriesDate > series2._data.seriesDate ||
|
||||||
|
(series1._data.seriesDate === series2._data.seriesDate &&
|
||||||
|
series1._data.seriesTime > series2._data.seriesTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export default findMostRecentStructuredReport;
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
/**
|
||||||
|
* Retrieve a list of all displaySets of all studies
|
||||||
|
*
|
||||||
|
* @param {Object} studies - List of studies loaded into the viewer
|
||||||
|
* @returns {Object} List of DisplaySets
|
||||||
|
*/
|
||||||
|
const getAllDisplaySets = (studies) => {
|
||||||
|
let allDisplaySets = [];
|
||||||
|
|
||||||
|
studies.forEach(study => {
|
||||||
|
if (study.getDisplaySets) {
|
||||||
|
allDisplaySets = allDisplaySets.concat(study.getDisplaySets());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return allDisplaySets;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default getAllDisplaySets;
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import * as dcmjs from 'dcmjs';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if dcmjs has support to determined tool
|
||||||
|
*
|
||||||
|
* @param {string} toolName
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
const isToolSupported = toolName => {
|
||||||
|
const adapter = dcmjs.adapters.Cornerstone;
|
||||||
|
return !!adapter[toolName];
|
||||||
|
};
|
||||||
|
|
||||||
|
export default isToolSupported;
|
||||||
@@ -876,7 +876,7 @@
|
|||||||
core-js "^2.6.5"
|
core-js "^2.6.5"
|
||||||
regenerator-runtime "^0.13.2"
|
regenerator-runtime "^0.13.2"
|
||||||
|
|
||||||
"@babel/polyfill@^7.0.0", "@babel/polyfill@^7.2.5":
|
"@babel/polyfill@^7.0.0", "@babel/polyfill@^7.6.0":
|
||||||
version "7.6.0"
|
version "7.6.0"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/polyfill/-/polyfill-7.6.0.tgz#6d89203f8b6cd323e8d946e47774ea35dc0619cc"
|
resolved "https://registry.yarnpkg.com/@babel/polyfill/-/polyfill-7.6.0.tgz#6d89203f8b6cd323e8d946e47774ea35dc0619cc"
|
||||||
integrity sha512-q5BZJI0n/B10VaQQvln1IlDK3BTBJFbADx7tv+oXDPIDZuTo37H5Adb9jhlXm/fEN4Y7/64qD9mnrJJG7rmaTw==
|
integrity sha512-q5BZJI0n/B10VaQQvln1IlDK3BTBJFbADx7tv+oXDPIDZuTo37H5Adb9jhlXm/fEN4Y7/64qD9mnrJJG7rmaTw==
|
||||||
@@ -6535,14 +6535,14 @@ dateformat@^3.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-3.0.3.tgz#a6e37499a4d9a9cf85ef5872044d62901c9889ae"
|
resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-3.0.3.tgz#a6e37499a4d9a9cf85ef5872044d62901c9889ae"
|
||||||
integrity sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==
|
integrity sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==
|
||||||
|
|
||||||
dcmjs@^0.6.1:
|
dcmjs@^0.8.2:
|
||||||
version "0.6.1"
|
version "0.8.2"
|
||||||
resolved "https://registry.yarnpkg.com/dcmjs/-/dcmjs-0.6.1.tgz#453f764bca2cd8e3430126ba8385ef4c77071bcd"
|
resolved "https://registry.yarnpkg.com/dcmjs/-/dcmjs-0.8.2.tgz#e1107273764de0a3052244b79d999fd870e58650"
|
||||||
integrity sha512-sJ0cRVZY02GSaAW5S9JPcv8+7iDtgrM0L/niYWAvu2piSa4bFSXwKi+t4XevAfkm+mF0sXZtaD15bw+/oiCYkw==
|
integrity sha512-HEd1Rq12qxZXVXr5B4xgl/foCWaXxioO5v42FLgiUjKrbQR6iKIVcZsmE+3/AvAurcHiQFfW5vk5K4AY7eAp5A==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/polyfill" "^7.2.5"
|
"@babel/polyfill" "^7.6.0"
|
||||||
"@babel/runtime" "^7.3.1"
|
"@babel/runtime" "^7.6.3"
|
||||||
loglevelnext "^3.0.0"
|
loglevelnext "^3.0.1"
|
||||||
ndarray "^1.0.18"
|
ndarray "^1.0.18"
|
||||||
|
|
||||||
debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.0, debug@^2.6.1, debug@^2.6.3, debug@^2.6.6, debug@^2.6.8, debug@^2.6.9:
|
debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.0, debug@^2.6.1, debug@^2.6.3, debug@^2.6.6, debug@^2.6.8, debug@^2.6.9:
|
||||||
@@ -12373,7 +12373,7 @@ loglevelnext@^1.0.1, loglevelnext@^1.0.2:
|
|||||||
es6-symbol "^3.1.1"
|
es6-symbol "^3.1.1"
|
||||||
object.assign "^4.1.0"
|
object.assign "^4.1.0"
|
||||||
|
|
||||||
loglevelnext@^3.0.0:
|
loglevelnext@^3.0.1:
|
||||||
version "3.0.1"
|
version "3.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/loglevelnext/-/loglevelnext-3.0.1.tgz#e3e4659c4061c09264f6812c33586dc55a009a04"
|
resolved "https://registry.yarnpkg.com/loglevelnext/-/loglevelnext-3.0.1.tgz#e3e4659c4061c09264f6812c33586dc55a009a04"
|
||||||
integrity sha512-JpjaJhIN1reaSb26SIxDGtE0uc67gPl19OMVHrr+Ggt6b/Vy60jmCtKgQBrygAH0bhRA2nkxgDvM+8QvR8r0YA==
|
integrity sha512-JpjaJhIN1reaSb26SIxDGtE0uc67gPl19OMVHrr+Ggt6b/Vy60jmCtKgQBrygAH0bhRA2nkxgDvM+8QvR8r0YA==
|
||||||
|
|||||||
Reference in new issue
Block a user