feat: New @ohif/viewer project structure + routes (#1674)
* fix pagination size * clean up, start a very basic route configuration * quick navigation between routes * configure webpack to read .env variables * Move container components to Routes directory * Fix naming of recipe file * Clean up source folder * update yarn.lock * ci: update netlify deploy preview to build viewer project * update PWA entrypoint to pass configuration on window to App as props * fix: for missing key * Pass routerBasename to our React Router component * Simplify config for meow Co-authored-by: dannyrb <danny.ri.brown@gmail.com>
This commit is contained in:
parent
9a8bede80e
commit
01b314522d
@ -12,9 +12,9 @@ yarn -v
|
|||||||
node -v
|
node -v
|
||||||
|
|
||||||
# Build && Move PWA Output
|
# Build && Move PWA Output
|
||||||
# yarn run build:ci
|
yarn run build:ci
|
||||||
# mkdir -p ./.netlify/www/pwa
|
mkdir -p ./.netlify/www/pwa
|
||||||
# mv platform/viewer/dist/* .netlify/www/pwa -v
|
mv platform/viewer/dist/* .netlify/www/pwa -v
|
||||||
|
|
||||||
# Build && Move script output
|
# Build && Move script output
|
||||||
# yarn run build:package
|
# yarn run build:package
|
||||||
@ -25,6 +25,3 @@ mkdir -p ./.netlify/www/ui
|
|||||||
mv platform/ui/.docz/dist/* .netlify/www/ui -v
|
mv platform/ui/.docz/dist/* .netlify/www/ui -v
|
||||||
|
|
||||||
echo 'Nothing left to see here. Go home, folks.'
|
echo 'Nothing left to see here. Go home, folks.'
|
||||||
|
|
||||||
# Build using react-scripts
|
|
||||||
# npx cross-env PUBLIC_URL=/demo APP_CONFIG=config/netlify.js react-scripts --max_old_space_size=4096 build
|
|
||||||
|
|||||||
@ -56,7 +56,7 @@ const position = {
|
|||||||
const getChildComponent = (childComponents, componentOpen) => {
|
const getChildComponent = (childComponents, componentOpen) => {
|
||||||
if (Array.isArray(childComponents)) {
|
if (Array.isArray(childComponents)) {
|
||||||
return childComponents.find(
|
return childComponents.find(
|
||||||
(_childComponent) => _childComponent.name === componentOpen
|
_childComponent => _childComponent.name === componentOpen
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return childComponents;
|
return childComponents;
|
||||||
@ -80,10 +80,10 @@ const SidePanel = ({
|
|||||||
const _childComponents = Array.isArray(childComponents)
|
const _childComponents = Array.isArray(childComponents)
|
||||||
? childComponents
|
? childComponents
|
||||||
: [childComponents];
|
: [childComponents];
|
||||||
return _childComponents.map((childComponent) => {
|
return _childComponents.map((childComponent, i) => {
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
key={childComponent.name}
|
key={i}
|
||||||
variant="text"
|
variant="text"
|
||||||
color="inherit"
|
color="inherit"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
|||||||
@ -49,25 +49,22 @@ const StudyListPagination = ({
|
|||||||
</Typography>
|
</Typography>
|
||||||
<ButtonGroup color="primary">
|
<ButtonGroup color="primary">
|
||||||
<IconButton
|
<IconButton
|
||||||
size="small"
|
size="inherit"
|
||||||
className="border-common-active px-4 py-2 text-base"
|
className="border-common-active px-4 py-2 text-base"
|
||||||
style={{ padding: '3px 12px', minWidth: 0 }}
|
|
||||||
color="white"
|
color="white"
|
||||||
onClick={() => navigateToPage(1)}
|
onClick={() => navigateToPage(1)}
|
||||||
>
|
>
|
||||||
<>{`<<`}</>
|
<>{`<<`}</>
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size="inherit"
|
||||||
className="border-common-active py-2 text-base"
|
className="border-common-active py-2 px-2 text-base"
|
||||||
style={{ padding: '3px 8px', minWidth: 0 }}
|
|
||||||
color="white"
|
color="white"
|
||||||
onClick={() => navigateToPage(currentPage - 1)}
|
onClick={() => navigateToPage(currentPage - 1)}
|
||||||
>{`< Previous`}</Button>
|
>{`< Previous`}</Button>
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size="inherit"
|
||||||
className="border-common-active py-2 text-base"
|
className="border-common-active py-2 px-4 text-base"
|
||||||
style={{ padding: '3px 20px', minWidth: 0 }}
|
|
||||||
color="white"
|
color="white"
|
||||||
onClick={() => navigateToPage(currentPage + 1)}
|
onClick={() => navigateToPage(currentPage + 1)}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -8,3 +8,4 @@
|
|||||||
|
|
||||||
PUBLIC_URL=/
|
PUBLIC_URL=/
|
||||||
APP_CONFIG=config/default.js
|
APP_CONFIG=config/default.js
|
||||||
|
USE_HASH_ROUTER=false
|
||||||
|
|||||||
@ -7,3 +7,4 @@
|
|||||||
|
|
||||||
PUBLIC_URL=/demo/
|
PUBLIC_URL=/demo/
|
||||||
APP_CONFIG=config/netlify.js
|
APP_CONFIG=config/netlify.js
|
||||||
|
USE_HASH_ROUTER=false
|
||||||
|
|||||||
@ -16,6 +16,7 @@ const PUBLIC_DIR = path.join(__dirname, '../public');
|
|||||||
const APP_CONFIG = process.env.APP_CONFIG || 'config/default.js';
|
const APP_CONFIG = process.env.APP_CONFIG || 'config/default.js';
|
||||||
const HTML_TEMPLATE = process.env.HTML_TEMPLATE || 'script-tag.html';
|
const HTML_TEMPLATE = process.env.HTML_TEMPLATE || 'script-tag.html';
|
||||||
const PUBLIC_URL = process.env.PUBLIC_URL || '/';
|
const PUBLIC_URL = process.env.PUBLIC_URL || '/';
|
||||||
|
const Dotenv = require('dotenv-webpack');
|
||||||
|
|
||||||
module.exports = (env, argv) => {
|
module.exports = (env, argv) => {
|
||||||
const commonConfig = webpackCommon(env, argv, { SRC_DIR, DIST_DIR });
|
const commonConfig = webpackCommon(env, argv, { SRC_DIR, DIST_DIR });
|
||||||
@ -34,6 +35,7 @@ module.exports = (env, argv) => {
|
|||||||
rules: [fontsToJavaScriptRule],
|
rules: [fontsToJavaScriptRule],
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
|
new Dotenv(),
|
||||||
// Clean output.path
|
// Clean output.path
|
||||||
new CleanWebpackPlugin(),
|
new CleanWebpackPlugin(),
|
||||||
new CopyWebpackPlugin([
|
new CopyWebpackPlugin([
|
||||||
|
|||||||
@ -27,6 +27,7 @@ const APP_CONFIG = process.env.APP_CONFIG || 'config/default.js';
|
|||||||
const PROXY_TARGET = process.env.PROXY_TARGET;
|
const PROXY_TARGET = process.env.PROXY_TARGET;
|
||||||
const PROXY_DOMAIN = process.env.PROXY_DOMAIN;
|
const PROXY_DOMAIN = process.env.PROXY_DOMAIN;
|
||||||
const ENTRY_TARGET = process.env.ENTRY_TARGET || `${SRC_DIR}/index.js`;
|
const ENTRY_TARGET = process.env.ENTRY_TARGET || `${SRC_DIR}/index.js`;
|
||||||
|
const Dotenv = require('dotenv-webpack');
|
||||||
|
|
||||||
module.exports = (env, argv) => {
|
module.exports = (env, argv) => {
|
||||||
const baseConfig = webpackBase(env, argv, { SRC_DIR, DIST_DIR });
|
const baseConfig = webpackBase(env, argv, { SRC_DIR, DIST_DIR });
|
||||||
@ -46,6 +47,7 @@ module.exports = (env, argv) => {
|
|||||||
rules: [...extractStyleChunksRule(isProdBuild)],
|
rules: [...extractStyleChunksRule(isProdBuild)],
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
|
new Dotenv(),
|
||||||
// Uncomment to generate bundle analyzer
|
// Uncomment to generate bundle analyzer
|
||||||
// new BundleAnalyzerPlugin(),
|
// new BundleAnalyzerPlugin(),
|
||||||
// Clean output.path
|
// Clean output.path
|
||||||
|
|||||||
@ -1,16 +0,0 @@
|
|||||||
Findings
|
|
||||||
|
|
||||||
New T2w-hyperintense lesions
|
|
||||||
Optic nerve
|
|
||||||
Optic nerve (right): Unremarkable.
|
|
||||||
Optic nerve (left): Unremarkable.
|
|
||||||
PML-typical changes
|
|
||||||
No.
|
|
||||||
Additional findings
|
|
||||||
Ventricular system: Normal width, midline position andsymmetry of the ventricular system for age.
|
|
||||||
Cerebral atrophy (visual): No signs of cerebral atrophy.
|
|
||||||
Corticomedullary differentiation: Preserved corticomedullary differentiation.
|
|
||||||
Sella and mastoid: Unremarkable.
|
|
||||||
Orbits: No orbital symmetry.
|
|
||||||
Paranasal sinuses: Clear.
|
|
||||||
|
|
||||||
@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "Using fixtures to represent data",
|
|
||||||
"email": "hello@cypress.io",
|
|
||||||
"body": "Fixtures are a great way to mock data for responses to routes"
|
|
||||||
}
|
|
||||||
@ -1,78 +0,0 @@
|
|||||||
const dataTransfer = new DataTransfer();
|
|
||||||
|
|
||||||
export const DragSimulator = {
|
|
||||||
MAX_TRIES: 1,
|
|
||||||
DELAY_INTERVAL_MS: 10,
|
|
||||||
counter: 0,
|
|
||||||
rectsEqual(r1, r2) {
|
|
||||||
return (
|
|
||||||
r1.top === r2.top &&
|
|
||||||
r1.right === r2.right &&
|
|
||||||
r1.bottom === r2.bottom &&
|
|
||||||
r1.left === r2.left
|
|
||||||
);
|
|
||||||
},
|
|
||||||
get dropped() {
|
|
||||||
const currentSourcePosition = this.source.getBoundingClientRect();
|
|
||||||
return !this.rectsEqual(
|
|
||||||
this.initialSourcePosition,
|
|
||||||
currentSourcePosition
|
|
||||||
);
|
|
||||||
},
|
|
||||||
get hasTriesLeft() {
|
|
||||||
return this.counter < this.MAX_TRIES;
|
|
||||||
},
|
|
||||||
dragstart() {
|
|
||||||
cy.log('**DRAG START**');
|
|
||||||
cy.wrap(this.source)
|
|
||||||
.trigger('mousedown', { which: 1, button: 0 })
|
|
||||||
.trigger('dragstart', { dataTransfer })
|
|
||||||
.trigger('drag', {});
|
|
||||||
},
|
|
||||||
drop() {
|
|
||||||
cy.log('**DROP**');
|
|
||||||
return cy
|
|
||||||
.wrap(this.target)
|
|
||||||
.trigger('mousemove', 'center')
|
|
||||||
.trigger('dragover', { dataTransfer, force: true })
|
|
||||||
.trigger('drop', { dataTransfer, force: true })
|
|
||||||
.trigger('dragend', { dataTransfer })
|
|
||||||
.trigger('mouseup', { which: 1, button: 0 });
|
|
||||||
},
|
|
||||||
dragover() {
|
|
||||||
cy.log('**DRAGOVER**');
|
|
||||||
if (!this.dropped && this.hasTriesLeft) {
|
|
||||||
this.counter += 1;
|
|
||||||
return cy
|
|
||||||
.wrap(this.target)
|
|
||||||
.trigger('mousemove', 'center')
|
|
||||||
.trigger('dragover', {
|
|
||||||
dataTransfer,
|
|
||||||
position: this.position,
|
|
||||||
})
|
|
||||||
.wait(this.DELAY_INTERVAL_MS)
|
|
||||||
.then(() => this.dragover());
|
|
||||||
}
|
|
||||||
return this.drop().then(() => true);
|
|
||||||
},
|
|
||||||
init(source, target, position) {
|
|
||||||
this.source = source;
|
|
||||||
this.target = target;
|
|
||||||
this.position = position;
|
|
||||||
this.counter = 0;
|
|
||||||
|
|
||||||
this.dragstart();
|
|
||||||
|
|
||||||
return cy.wait(this.DELAY_INTERVAL_MS).then(() => {
|
|
||||||
this.initialSourcePosition = this.source.getBoundingClientRect();
|
|
||||||
return this.dragover();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
simulate(sourceWrapper, targetSelector, position = 'center') {
|
|
||||||
return cy
|
|
||||||
.get(targetSelector)
|
|
||||||
.then(targetWrapper =>
|
|
||||||
this.init(sourceWrapper.get(0), targetWrapper.get(0), position)
|
|
||||||
);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@ -1,31 +0,0 @@
|
|||||||
describe('OHIF Load Local File', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
cy.visit('/local');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('checks if user can navigate to /local', () => {
|
|
||||||
cy.get('.drag-drop-contents').should(
|
|
||||||
'contain',
|
|
||||||
'Drag and Drop DICOM files here to load them in the Viewer'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('loads an invalid file and verify if viewer is empty', () => {
|
|
||||||
const fileName = 'example.json';
|
|
||||||
|
|
||||||
cy.fixture(fileName).then(fileContent => {
|
|
||||||
cy.get('input[type=file]')
|
|
||||||
.first()
|
|
||||||
.upload({
|
|
||||||
fileContent,
|
|
||||||
fileName,
|
|
||||||
mimeType: 'application/json',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
//Verify if there is no thumbnail visible
|
|
||||||
cy.expectMinimumThumbnails(0);
|
|
||||||
//Verify if DOC thumnails is displayed and load it on viewport
|
|
||||||
cy.get('[data-cy="thumbnail-list"]').should('not.exist');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -1,27 +0,0 @@
|
|||||||
describe('OHIF Microscopy Extension', () => {
|
|
||||||
before(() => {
|
|
||||||
cy.openStudyModality('SM');
|
|
||||||
cy.expectMinimumThumbnails(6);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('checks if series thumbnails are being displayed', () => {
|
|
||||||
cy.get('[data-cy="thumbnail-list"]')
|
|
||||||
.contains('SM')
|
|
||||||
.its('length')
|
|
||||||
.should('to.be.at.least', 1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('drags and drop a SM thumbnail into viewport', () => {
|
|
||||||
cy.get('[data-cy="thumbnail-list"]')
|
|
||||||
.contains('SM')
|
|
||||||
.drag('.viewport-drop-target');
|
|
||||||
|
|
||||||
cy.get('.DicomMicroscopyViewer')
|
|
||||||
.its('length')
|
|
||||||
.should('be.eq', 1);
|
|
||||||
|
|
||||||
cy.wait(3000); //Waiting for image to render before taking the snapshot
|
|
||||||
// Visual comparison
|
|
||||||
cy.screenshot('Microscopy Extension - Should display loaded canvas');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -1,29 +0,0 @@
|
|||||||
describe('OHIF Load Local PDF File', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
cy.visit('/local');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('loads a PDF DICOM file and visualize it', () => {
|
|
||||||
const fileName = 'PDFDICOMfile.pdf';
|
|
||||||
|
|
||||||
cy.fixture(fileName).then(fileContent => {
|
|
||||||
cy.get('input[type=file]')
|
|
||||||
.first()
|
|
||||||
.upload({
|
|
||||||
fileContent,
|
|
||||||
fileName,
|
|
||||||
mimeType: 'application/pdf',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
//Verify if 1 thumbnail is visible
|
|
||||||
cy.expectMinimumThumbnails(1);
|
|
||||||
//Verify if DOC thumnails is displayed and load it on viewport
|
|
||||||
cy.get('[data-cy="thumbnail-list"]')
|
|
||||||
.contains('DOC')
|
|
||||||
.click({ force: true });
|
|
||||||
|
|
||||||
//Verify if PDF document is displayed on the viewport
|
|
||||||
cy.get('#pdf-canvas-container').should('be.visible');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -1,22 +0,0 @@
|
|||||||
describe('Visual Regression - OHIF Microscopy Extension', () => {
|
|
||||||
before(() => {
|
|
||||||
cy.openStudyModality('SM');
|
|
||||||
cy.expectMinimumThumbnails(6);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('drags and drop a SM thumbnail into viewport', () => {
|
|
||||||
cy.get('[data-cy="thumbnail-list"]')
|
|
||||||
.contains('SM')
|
|
||||||
.drag('.viewport-drop-target');
|
|
||||||
|
|
||||||
cy.get('.DicomMicroscopyViewer')
|
|
||||||
.its('length')
|
|
||||||
.should('be.eq', 1);
|
|
||||||
|
|
||||||
cy.wait(3000); //Waiting for image to render before taking the snapshot
|
|
||||||
// Visual comparison
|
|
||||||
cy.percyCanvasSnapshot(
|
|
||||||
'Microscopy Extension - Should display loaded canvas'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -1,35 +0,0 @@
|
|||||||
// ***********************************************************
|
|
||||||
// This example plugins/index.js can be used to load plugins
|
|
||||||
//
|
|
||||||
// You can change the location of this file or turn off loading
|
|
||||||
// the plugins file with the 'pluginsFile' configuration option.
|
|
||||||
//
|
|
||||||
// You can read more here:
|
|
||||||
// https://on.cypress.io/plugins-guide
|
|
||||||
// ***********************************************************
|
|
||||||
let percyHealthCheck = require('@percy/cypress/task');
|
|
||||||
|
|
||||||
// This function is called when a project is opened or re-opened (e.g. due to
|
|
||||||
// the project's config changing)
|
|
||||||
module.exports = (on, config) => {
|
|
||||||
// `on` is used to hook into various events Cypress emits
|
|
||||||
// `config` is the resolved Cypress config
|
|
||||||
on('before:browser:launch', (browser = {}, args) => {
|
|
||||||
if (browser.name === 'chrome') {
|
|
||||||
// `args` is an araay of all the arguments
|
|
||||||
// that will be passed to Chrome when it launchers
|
|
||||||
args.push('--start-fullscreen');
|
|
||||||
|
|
||||||
// whatever you return here becomes the new args
|
|
||||||
return args;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (browser.name === 'chromium') {
|
|
||||||
const newArgs = args.filter(arg => arg !== '--disable-gpu');
|
|
||||||
newArgs.push('--ignore-gpu-blacklist');
|
|
||||||
return newArgs;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
on('task', percyHealthCheck);
|
|
||||||
};
|
|
||||||
@ -1,35 +0,0 @@
|
|||||||
// ***********************************************************
|
|
||||||
// This example support/index.js is processed and
|
|
||||||
// loaded automatically before your test files.
|
|
||||||
//
|
|
||||||
// This is a great place to put global configuration and
|
|
||||||
// behavior that modifies Cypress.
|
|
||||||
//
|
|
||||||
// You can change the location of this file or turn off
|
|
||||||
// automatically serving support files with the
|
|
||||||
// 'supportFile' configuration option.
|
|
||||||
//
|
|
||||||
// You can read more here:
|
|
||||||
// https://on.cypress.io/configuration
|
|
||||||
// ***********************************************************
|
|
||||||
|
|
||||||
import './commands';
|
|
||||||
|
|
||||||
Cypress.on('window:before:load', window => {
|
|
||||||
// Override our `getContext` function so all contexts that are webGl are
|
|
||||||
// Created with `preserveDrawingBuffer = true`; this is required for percy
|
|
||||||
// Snapshot
|
|
||||||
window.HTMLCanvasElement.prototype.getContext = (function(oldGetContextFn) {
|
|
||||||
return function(type, attrs) {
|
|
||||||
attrs = attrs || {};
|
|
||||||
if (
|
|
||||||
type === 'webgl' ||
|
|
||||||
type === 'webgl2' ||
|
|
||||||
type === 'experimental-webgl'
|
|
||||||
) {
|
|
||||||
attrs.preserveDrawingBuffer = true;
|
|
||||||
}
|
|
||||||
return oldGetContextFn.apply(this, [type, attrs]);
|
|
||||||
};
|
|
||||||
})(HTMLCanvasElement.prototype.getContext);
|
|
||||||
});
|
|
||||||
@ -71,6 +71,7 @@
|
|||||||
"dcmjs": "^0.12.2",
|
"dcmjs": "^0.12.2",
|
||||||
"dicom-parser": "^1.8.3",
|
"dicom-parser": "^1.8.3",
|
||||||
"dicomweb-client": "^0.4.4",
|
"dicomweb-client": "^0.4.4",
|
||||||
|
"dotenv-webpack": "^1.7.0",
|
||||||
"hammerjs": "^2.0.8",
|
"hammerjs": "^2.0.8",
|
||||||
"i18next": "^17.0.3",
|
"i18next": "^17.0.3",
|
||||||
"i18next-browser-languagedetector": "^3.0.1",
|
"i18next-browser-languagedetector": "^3.0.1",
|
||||||
|
|||||||
@ -16,101 +16,6 @@ window.config = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
hotkeys: [
|
|
||||||
// ~ Global
|
|
||||||
{
|
|
||||||
commandName: 'incrementActiveViewport',
|
|
||||||
label: 'Next Viewport',
|
|
||||||
keys: ['right'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
commandName: 'decrementActiveViewport',
|
|
||||||
label: 'Previous Viewport',
|
|
||||||
keys: ['left'],
|
|
||||||
},
|
|
||||||
// Supported Keys: https://craig.is/killing/mice
|
|
||||||
// ~ Cornerstone Extension
|
|
||||||
{ commandName: 'rotateViewportCW', label: 'Rotate Right', keys: ['r'] },
|
|
||||||
{ commandName: 'rotateViewportCCW', label: 'Rotate Left', keys: ['l'] },
|
|
||||||
{ commandName: 'invertViewport', label: 'Invert', keys: ['i'] },
|
|
||||||
{
|
|
||||||
commandName: 'flipViewportVertical',
|
|
||||||
label: 'Flip Horizontally',
|
|
||||||
keys: ['h'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
commandName: 'flipViewportHorizontal',
|
|
||||||
label: 'Flip Vertically',
|
|
||||||
keys: ['v'],
|
|
||||||
},
|
|
||||||
{ commandName: 'scaleUpViewport', label: 'Zoom In', keys: ['+'] },
|
|
||||||
{ commandName: 'scaleDownViewport', label: 'Zoom Out', keys: ['-'] },
|
|
||||||
{ commandName: 'fitViewportToWindow', label: 'Zoom to Fit', keys: ['='] },
|
|
||||||
{ commandName: 'resetViewport', label: 'Reset', keys: ['space'] },
|
|
||||||
// clearAnnotations
|
|
||||||
{ commandName: 'nextImage', label: 'Next Image', keys: ['down'] },
|
|
||||||
{ commandName: 'previousImage', label: 'Previous Image', keys: ['up'] },
|
|
||||||
// firstImage
|
|
||||||
// lastImage
|
|
||||||
{
|
|
||||||
commandName: 'previousViewportDisplaySet',
|
|
||||||
label: 'Previous Series',
|
|
||||||
keys: ['pagedown'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
commandName: 'nextViewportDisplaySet',
|
|
||||||
label: 'Next Series',
|
|
||||||
keys: ['pageup'],
|
|
||||||
},
|
|
||||||
// ~ Cornerstone Tools
|
|
||||||
{ commandName: 'setZoomTool', label: 'Zoom', keys: ['z'] },
|
|
||||||
// ~ Window level presets
|
|
||||||
{
|
|
||||||
commandName: 'windowLevelPreset1',
|
|
||||||
label: 'W/L Preset 1',
|
|
||||||
keys: ['1'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
commandName: 'windowLevelPreset2',
|
|
||||||
label: 'W/L Preset 2',
|
|
||||||
keys: ['2'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
commandName: 'windowLevelPreset3',
|
|
||||||
label: 'W/L Preset 3',
|
|
||||||
keys: ['3'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
commandName: 'windowLevelPreset4',
|
|
||||||
label: 'W/L Preset 4',
|
|
||||||
keys: ['4'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
commandName: 'windowLevelPreset5',
|
|
||||||
label: 'W/L Preset 5',
|
|
||||||
keys: ['5'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
commandName: 'windowLevelPreset6',
|
|
||||||
label: 'W/L Preset 6',
|
|
||||||
keys: ['6'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
commandName: 'windowLevelPreset7',
|
|
||||||
label: 'W/L Preset 7',
|
|
||||||
keys: ['7'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
commandName: 'windowLevelPreset8',
|
|
||||||
label: 'W/L Preset 8',
|
|
||||||
keys: ['8'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
commandName: 'windowLevelPreset9',
|
|
||||||
label: 'W/L Preset 9',
|
|
||||||
keys: ['9'],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
i18n: {
|
i18n: {
|
||||||
LOCIZE_PROJECTID: 'a8da3f9a-e467-4dd6-af33-474d582a0294',
|
LOCIZE_PROJECTID: 'a8da3f9a-e467-4dd6-af33-474d582a0294',
|
||||||
LOCIZE_API_KEY: null, // Developers can use this to do in-context editing. DO NOT COMMIT THIS KEY!
|
LOCIZE_API_KEY: null, // Developers can use this to do in-context editing. DO NOT COMMIT THIS KEY!
|
||||||
|
|||||||
@ -1,14 +1,46 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { BrowserRouter, HashRouter } from 'react-router-dom';
|
||||||
import { ThemeWrapper } from '@ohif/ui';
|
import { ThemeWrapper } from '@ohif/ui';
|
||||||
|
|
||||||
import ConnectedStudyList from './connectedComponents/ConnectedStudyList';
|
import routes from './routes';
|
||||||
|
|
||||||
|
function App({ config }) {
|
||||||
|
const { routerBasename } = config;
|
||||||
|
const Router = JSON.parse(process.env.USE_HASH_ROUTER)
|
||||||
|
? HashRouter
|
||||||
|
: BrowserRouter;
|
||||||
|
|
||||||
const App = () => {
|
|
||||||
return (
|
return (
|
||||||
<ThemeWrapper>
|
<Router basename={routerBasename}>
|
||||||
<ConnectedStudyList />
|
<ThemeWrapper>{routes()}</ThemeWrapper>
|
||||||
</ThemeWrapper>
|
</Router>
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
App.propTypes = {
|
||||||
|
config: PropTypes.oneOfType([
|
||||||
|
PropTypes.func,
|
||||||
|
PropTypes.shape({
|
||||||
|
routerBasename: PropTypes.string.isRequired,
|
||||||
|
oidc: PropTypes.array,
|
||||||
|
whiteLabeling: PropTypes.shape({
|
||||||
|
createLogoComponentFn: PropTypes.func,
|
||||||
|
}),
|
||||||
|
extensions: PropTypes.array,
|
||||||
|
}),
|
||||||
|
]).isRequired,
|
||||||
|
/* Extensions that are "bundled" or "baked-in" to the application */
|
||||||
|
defaultExtensions: PropTypes.array,
|
||||||
|
};
|
||||||
|
|
||||||
|
App.defaultProps = {
|
||||||
|
config: {
|
||||||
|
showStudyList: true,
|
||||||
|
oidc: [],
|
||||||
|
extensions: [],
|
||||||
|
},
|
||||||
|
defaultExtensions: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
|
|||||||
@ -1,37 +0,0 @@
|
|||||||
html,
|
|
||||||
body,
|
|
||||||
#root {
|
|
||||||
background-color: black;
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
box-sizing: border-box;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
font-family: Roboto, OpenSans, HelveticaNeue-Light, Helvetica Neue Light,
|
|
||||||
Helvetica Neue, Helvetica, Arial, Lucida Grande, sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 1.42857143;
|
|
||||||
}
|
|
||||||
|
|
||||||
#root .fade-enter {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#root .fade-enter-active {
|
|
||||||
opacity: 1;
|
|
||||||
transition: opacity 200ms;
|
|
||||||
}
|
|
||||||
|
|
||||||
#root .fade-exit {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
#root .fade-exit-active {
|
|
||||||
opacity: 0;
|
|
||||||
transition: opacity 200ms;
|
|
||||||
}
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
import commandsModule from './commandsModule.js';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
id: 'generic-viewer-commands',
|
|
||||||
getCommandsModule({ commandsManager }) {
|
|
||||||
return commandsModule({ commandsManager });
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@ -1,420 +0,0 @@
|
|||||||
import { connect } from 'react-redux';
|
|
||||||
import { MeasurementTable } from '@ohif/ui';
|
|
||||||
import OHIF, { DICOMSR } from '@ohif/core';
|
|
||||||
import moment from 'moment';
|
|
||||||
import cornerstone from 'cornerstone-core';
|
|
||||||
|
|
||||||
import jumpToRowItem from './jumpToRowItem.js';
|
|
||||||
|
|
||||||
const { setViewportSpecificData } = OHIF.redux.actions;
|
|
||||||
const { MeasurementApi } = OHIF.measurements;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Takes a list of objects and a property and return the list grouped by the property
|
|
||||||
*
|
|
||||||
* @param {Array} list - The objects to be grouped by
|
|
||||||
* @param {string} props - The property to group the objects
|
|
||||||
* @returns {Object}
|
|
||||||
*/
|
|
||||||
function groupBy(list, props) {
|
|
||||||
return list.reduce((a, b) => {
|
|
||||||
(a[b[props]] = a[b[props]] || []).push(b);
|
|
||||||
return a;
|
|
||||||
}, {});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 = [];
|
|
||||||
toolGroups.forEach(toolGroup => (tools = tools.concat(toolGroup.childTools)));
|
|
||||||
|
|
||||||
return tools;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Takes measurementData and build the measurement text to be used into the table
|
|
||||||
*
|
|
||||||
* @param {Object} [measurementData={}]
|
|
||||||
* @param {string} measurementData.location - The measurement location
|
|
||||||
* @param {string} measurementData.description - The measurement description
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
function getMeasurementText(measurementData = {}) {
|
|
||||||
const defaultText = '...';
|
|
||||||
const { location = '', description = '' } = measurementData;
|
|
||||||
const result = location + (description ? ` (${description})` : '');
|
|
||||||
|
|
||||||
return result || defaultText;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Takes a list of measurements grouped by measurement numbers and return each measurement data by available timepoint
|
|
||||||
*
|
|
||||||
* @param {Array} measurementNumberList - The list of measurements
|
|
||||||
* @param {Array} timepoints - The list of available timepoints
|
|
||||||
* @param {Function} displayFunction - The function that builds the display text by each tool
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
function getDataForEachMeasurementNumber(
|
|
||||||
measurementNumberList,
|
|
||||||
timepoints,
|
|
||||||
displayFunction
|
|
||||||
) {
|
|
||||||
const data = [];
|
|
||||||
// on each measurement number we should get each measurement data by available timepoint
|
|
||||||
measurementNumberList.forEach(measurement => {
|
|
||||||
timepoints.forEach(timepoint => {
|
|
||||||
const eachData = {
|
|
||||||
displayText: '...',
|
|
||||||
};
|
|
||||||
if (measurement.timepointId === timepoint.timepointId) {
|
|
||||||
eachData.displayText = displayFunction(measurement);
|
|
||||||
}
|
|
||||||
data.push(eachData);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Take a measurement toolName and return if any warnings
|
|
||||||
*
|
|
||||||
* @param {string} toolName - The tool name
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
function getWarningsForMeasurement(toolName) {
|
|
||||||
const isToolSupported = DICOMSR.isToolSupported(toolName);
|
|
||||||
|
|
||||||
return {
|
|
||||||
hasWarnings: !isToolSupported,
|
|
||||||
warningTitle: isToolSupported ? '' : 'Unsupported Tool',
|
|
||||||
warningList: isToolSupported
|
|
||||||
? []
|
|
||||||
: [`${toolName} cannot be persisted at this time`],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Take measurements from MeasurementAPI structure and convert into a measurementTable structure
|
|
||||||
*
|
|
||||||
* @param {Object} toolCollections - The list of all measurement grouped by groupTool and toolName
|
|
||||||
* @param {Array} timepoints - The list of available timepoints
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
function convertMeasurementsToTableData(toolCollections, timepoints) {
|
|
||||||
const config = OHIF.measurements.MeasurementApi.getConfiguration();
|
|
||||||
const toolGroups = config.measurementTools;
|
|
||||||
const tools = getAllTools(toolGroups);
|
|
||||||
|
|
||||||
const tableMeasurements = toolGroups.map(toolGroup => {
|
|
||||||
return {
|
|
||||||
groupName: toolGroup.name,
|
|
||||||
groupId: toolGroup.id,
|
|
||||||
measurements: [],
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
Object.keys(toolCollections).forEach(toolId => {
|
|
||||||
const toolMeasurements = toolCollections[toolId];
|
|
||||||
const tool = tools.find(tool => tool.id === toolId);
|
|
||||||
const { displayFunction } = tool.options.measurementTable;
|
|
||||||
|
|
||||||
// Group by measurementNumber so we can display then all in the same line
|
|
||||||
const groupedMeasurements = groupBy(toolMeasurements, 'measurementNumber');
|
|
||||||
|
|
||||||
Object.keys(groupedMeasurements).forEach(groupedMeasurementsIndex => {
|
|
||||||
const measurementNumberList =
|
|
||||||
groupedMeasurements[groupedMeasurementsIndex];
|
|
||||||
const measurementData = measurementNumberList[0];
|
|
||||||
const {
|
|
||||||
measurementNumber,
|
|
||||||
lesionNamingNumber,
|
|
||||||
toolType,
|
|
||||||
} = measurementData;
|
|
||||||
const measurementId = measurementData._id;
|
|
||||||
|
|
||||||
const {
|
|
||||||
hasWarnings,
|
|
||||||
warningTitle,
|
|
||||||
warningList,
|
|
||||||
} = getWarningsForMeasurement(toolType);
|
|
||||||
|
|
||||||
//check if all measurements with same measurementNumber will have same LABEL
|
|
||||||
const tableMeasurement = {
|
|
||||||
itemNumber: lesionNamingNumber,
|
|
||||||
label: getMeasurementText(measurementData),
|
|
||||||
measurementId,
|
|
||||||
measurementNumber,
|
|
||||||
lesionNamingNumber,
|
|
||||||
toolType,
|
|
||||||
hasWarnings,
|
|
||||||
warningTitle,
|
|
||||||
warningList,
|
|
||||||
isSplitLesion: false, //TODO
|
|
||||||
data: getDataForEachMeasurementNumber(
|
|
||||||
measurementNumberList,
|
|
||||||
timepoints,
|
|
||||||
displayFunction
|
|
||||||
),
|
|
||||||
};
|
|
||||||
|
|
||||||
// find the group object for the tool
|
|
||||||
const toolGroupMeasurements = tableMeasurements.find(group => {
|
|
||||||
return group.groupId === tool.toolGroup;
|
|
||||||
});
|
|
||||||
// inject the new measurement for this measurementNumer
|
|
||||||
toolGroupMeasurements.measurements.push(tableMeasurement);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Sort measurements by lesion naming number
|
|
||||||
tableMeasurements.forEach(tm => {
|
|
||||||
tm.measurements.sort((m1, m2) =>
|
|
||||||
m1.lesionNamingNumber > m2.lesionNamingNumber ? 1 : -1
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
return tableMeasurements;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Take a list of available timepoints and return a list header information for each timepoint
|
|
||||||
*
|
|
||||||
* @param {Array} timepoints - The list of available timepoints
|
|
||||||
* @param {string} timepoints[].latestDate - The date of the last study taken on the timepoint
|
|
||||||
* @returns {{label: string, key: string, date: string}[]}
|
|
||||||
*/
|
|
||||||
function convertTimepointsToTableData(timepoints) {
|
|
||||||
if (!timepoints || !timepoints.length) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
label: 'Study date:',
|
|
||||||
key: 'StudyDate',
|
|
||||||
date: moment(timepoints[0].latestDate).format('DD-MMM-YY'),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 { timepointManager, servers } = state;
|
|
||||||
const { timepoints, measurements } = timepointManager;
|
|
||||||
const activeServer = servers.servers.find(a => a.active === true);
|
|
||||||
const saveFunction = getSaveFunction(activeServer.type);
|
|
||||||
|
|
||||||
return {
|
|
||||||
timepoints: convertTimepointsToTableData(timepoints),
|
|
||||||
measurementCollection: convertMeasurementsToTableData(
|
|
||||||
measurements,
|
|
||||||
timepoints
|
|
||||||
),
|
|
||||||
timepointManager: state.timepointManager,
|
|
||||||
viewports: state.viewports,
|
|
||||||
saveFunction,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch, ownProps) => {
|
|
||||||
return {
|
|
||||||
dispatchRelabel: (event, measurementData, viewportsState) => {
|
|
||||||
event.persist();
|
|
||||||
|
|
||||||
const activeViewportIndex =
|
|
||||||
(viewportsState && viewportsState.activeViewportIndex) || 0;
|
|
||||||
|
|
||||||
const enabledElements = cornerstone.getEnabledElements();
|
|
||||||
if (!enabledElements || enabledElements.length <= activeViewportIndex) {
|
|
||||||
OHIF.log.error('Failed to find the enabled element');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { toolType, measurementId } = measurementData;
|
|
||||||
const tool = MeasurementApi.Instance.tools[toolType].find(measurement => {
|
|
||||||
return measurement._id === measurementId;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Clone the tool not to set empty location initially
|
|
||||||
const toolForLocation = Object.assign({}, tool, { location: null });
|
|
||||||
|
|
||||||
if (ownProps.onRelabel) {
|
|
||||||
ownProps.onRelabel(toolForLocation);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
dispatchEditDescription: (event, measurementData, viewportsState) => {
|
|
||||||
event.persist();
|
|
||||||
|
|
||||||
const activeViewportIndex =
|
|
||||||
(viewportsState && viewportsState.activeViewportIndex) || 0;
|
|
||||||
|
|
||||||
const enabledElements = cornerstone.getEnabledElements();
|
|
||||||
if (!enabledElements || enabledElements.length <= activeViewportIndex) {
|
|
||||||
OHIF.log.error('Failed to find the enabled element');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { toolType, measurementId } = measurementData;
|
|
||||||
const tool = MeasurementApi.Instance.tools[toolType].find(measurement => {
|
|
||||||
return measurement._id === measurementId;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (ownProps.onEditDescription) {
|
|
||||||
ownProps.onEditDescription(tool);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
dispatchJumpToRowItem: (
|
|
||||||
measurementData,
|
|
||||||
viewportsState,
|
|
||||||
timepointManagerState,
|
|
||||||
options
|
|
||||||
) => {
|
|
||||||
const actionData = jumpToRowItem(
|
|
||||||
measurementData,
|
|
||||||
viewportsState,
|
|
||||||
timepointManagerState,
|
|
||||||
dispatch,
|
|
||||||
options
|
|
||||||
);
|
|
||||||
|
|
||||||
actionData.viewportSpecificData.forEach(viewportSpecificData => {
|
|
||||||
const { viewportIndex, displaySet } = viewportSpecificData;
|
|
||||||
|
|
||||||
dispatch(setViewportSpecificData(viewportIndex, displaySet));
|
|
||||||
});
|
|
||||||
|
|
||||||
const { toolType, measurementNumber } = measurementData;
|
|
||||||
const measurementApi = MeasurementApi.Instance;
|
|
||||||
|
|
||||||
Object.keys(measurementApi.tools).forEach(toolType => {
|
|
||||||
const measurements = measurementApi.tools[toolType];
|
|
||||||
|
|
||||||
measurements.forEach(measurement => {
|
|
||||||
measurement.active = false;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const measurementsToActive = measurementApi.tools[toolType].filter(
|
|
||||||
measurement => {
|
|
||||||
return measurement.measurementNumber === measurementNumber;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
measurementsToActive.forEach(measurementToActive => {
|
|
||||||
measurementToActive.active = true;
|
|
||||||
});
|
|
||||||
|
|
||||||
measurementApi.syncMeasurementsAndToolData();
|
|
||||||
|
|
||||||
cornerstone.getEnabledElements().forEach(enabledElement => {
|
|
||||||
cornerstone.updateImage(enabledElement.element);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Needs to update viewports.layout state to set layout
|
|
||||||
//const layout = actionData.layout;
|
|
||||||
//dispatch(setLayout(layout))
|
|
||||||
|
|
||||||
// Needs to update viewports.activeViewportIndex to the first updated viewport
|
|
||||||
//const viewportIndex = actionData.viewportIndex;
|
|
||||||
//dispatch(setViewportActive(viewportIndex));
|
|
||||||
|
|
||||||
// Needs to update timepointsManager.measurements state to set active measurementId
|
|
||||||
// TODO: Not yet implemented
|
|
||||||
//dispatch(setActiveMeasurement(measurementData.measurementId))
|
|
||||||
|
|
||||||
// (later): Needs to set some property on state.extensions.cornerstone to synchronize viewport scrolling
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const mergeProps = (propsFromState, propsFromDispatch, ownProps) => {
|
|
||||||
const { timepoints, saveFunction, measurementCollection } = propsFromState;
|
|
||||||
const { onSaveComplete, selectedMeasurementNumber } = ownProps;
|
|
||||||
|
|
||||||
return {
|
|
||||||
timepoints,
|
|
||||||
saveFunction,
|
|
||||||
measurementCollection,
|
|
||||||
onSaveComplete,
|
|
||||||
selectedMeasurementNumber,
|
|
||||||
...propsFromDispatch,
|
|
||||||
onItemClick: (event, measurementData) => {
|
|
||||||
// TODO: Add timepointId to .data for measurementData?
|
|
||||||
// TODO: Tooltype should be on the level below? This should
|
|
||||||
// provide the entire row item?
|
|
||||||
|
|
||||||
const viewportsState = propsFromState.viewports;
|
|
||||||
const timepointManagerState = propsFromState.timepointManager;
|
|
||||||
|
|
||||||
// TODO: invertViewportTimepointsOrder should be stored in / read from user preferences
|
|
||||||
// TODO: childToolKey should come from the measurement table when it supports child tools
|
|
||||||
const options = {
|
|
||||||
invertViewportTimepointsOrder: false,
|
|
||||||
childToolKey: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
propsFromDispatch.dispatchJumpToRowItem(
|
|
||||||
measurementData,
|
|
||||||
viewportsState,
|
|
||||||
timepointManagerState,
|
|
||||||
options
|
|
||||||
);
|
|
||||||
},
|
|
||||||
onRelabelClick: (event, measurementData) => {
|
|
||||||
const viewportsState = propsFromState.viewports;
|
|
||||||
propsFromDispatch.dispatchRelabel(event, measurementData, viewportsState);
|
|
||||||
},
|
|
||||||
onEditDescriptionClick: (event, measurementData) => {
|
|
||||||
const viewportsState = propsFromState.viewports;
|
|
||||||
propsFromDispatch.dispatchEditDescription(
|
|
||||||
event,
|
|
||||||
measurementData,
|
|
||||||
viewportsState
|
|
||||||
);
|
|
||||||
},
|
|
||||||
onDeleteClick: (event, measurementData) => {
|
|
||||||
const { MeasurementHandlers } = OHIF.measurements;
|
|
||||||
|
|
||||||
MeasurementHandlers.onRemoved({
|
|
||||||
detail: {
|
|
||||||
toolType: measurementData.toolType,
|
|
||||||
measurementData: {
|
|
||||||
_id: measurementData.measurementId,
|
|
||||||
lesionNamingNumber: measurementData.lesionNamingNumber,
|
|
||||||
measurementNumber: measurementData.measurementNumber,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const ConnectedMeasurementTable = connect(
|
|
||||||
mapStateToProps,
|
|
||||||
mapDispatchToProps,
|
|
||||||
mergeProps
|
|
||||||
)(MeasurementTable);
|
|
||||||
|
|
||||||
export default ConnectedMeasurementTable;
|
|
||||||
@ -1,87 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import ConnectedMeasurementTable from './ConnectedMeasurementTable.js';
|
|
||||||
import init from './init.js';
|
|
||||||
|
|
||||||
import LabellingFlow from '../../components/Labelling/LabellingFlow';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
/**
|
|
||||||
* Only required property. Should be a unique value across all extensions.
|
|
||||||
*/
|
|
||||||
id: 'measurements-table',
|
|
||||||
|
|
||||||
preRegistration({ servicesManager, commandsManager, configuration = {} }) {
|
|
||||||
init({ servicesManager, commandsManager, configuration });
|
|
||||||
},
|
|
||||||
|
|
||||||
getPanelModule({ servicesManager, commandsManager }) {
|
|
||||||
const { UINotificationService, UIDialogService } = servicesManager.services;
|
|
||||||
|
|
||||||
const showLabellingDialog = (props, measurementData) => {
|
|
||||||
if (!UIDialogService) {
|
|
||||||
console.warn('Unable to show dialog; no UI Dialog Service available.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
UIDialogService.dismiss({ id: 'labelling' });
|
|
||||||
UIDialogService.create({
|
|
||||||
id: 'labelling',
|
|
||||||
centralize: true,
|
|
||||||
isDraggable: false,
|
|
||||||
showOverlay: true,
|
|
||||||
content: LabellingFlow,
|
|
||||||
contentProps: {
|
|
||||||
measurementData,
|
|
||||||
labellingDoneCallback: () =>
|
|
||||||
UIDialogService.dismiss({ id: 'labelling' }),
|
|
||||||
updateLabelling: ({ location, description, response }) => {
|
|
||||||
measurementData.location = location || measurementData.location;
|
|
||||||
measurementData.description = description || '';
|
|
||||||
measurementData.response = response || measurementData.response;
|
|
||||||
|
|
||||||
commandsManager.runCommand(
|
|
||||||
'updateTableWithNewMeasurementData',
|
|
||||||
measurementData
|
|
||||||
);
|
|
||||||
},
|
|
||||||
...props,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const ExtendedConnectedMeasurementTable = () => (
|
|
||||||
<ConnectedMeasurementTable
|
|
||||||
onRelabel={tool =>
|
|
||||||
showLabellingDialog(
|
|
||||||
{ editLocation: true, skipAddLabelButton: true },
|
|
||||||
tool
|
|
||||||
)
|
|
||||||
}
|
|
||||||
onEditDescription={tool =>
|
|
||||||
showLabellingDialog({ editDescriptionOnDialog: true }, tool)
|
|
||||||
}
|
|
||||||
onSaveComplete={message => {
|
|
||||||
if (UINotificationService) {
|
|
||||||
UINotificationService.show(message);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
return {
|
|
||||||
menuOptions: [
|
|
||||||
{
|
|
||||||
icon: 'list',
|
|
||||||
label: 'Measurements',
|
|
||||||
target: 'measurement-panel',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
components: [
|
|
||||||
{
|
|
||||||
id: 'measurement-panel',
|
|
||||||
component: ExtendedConnectedMeasurementTable,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
defaultContext: ['VIEWER'],
|
|
||||||
};
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@ -1,251 +0,0 @@
|
|||||||
import OHIF from '@ohif/core';
|
|
||||||
import cornerstone from 'cornerstone-core';
|
|
||||||
import csTools from 'cornerstone-tools';
|
|
||||||
import throttle from 'lodash.throttle';
|
|
||||||
|
|
||||||
import LabellingFlow from '../../components/Labelling/LabellingFlow';
|
|
||||||
import ToolContextMenu from '../../connectedComponents/ToolContextMenu';
|
|
||||||
|
|
||||||
const {
|
|
||||||
onAdded,
|
|
||||||
onRemoved,
|
|
||||||
onModified,
|
|
||||||
} = OHIF.measurements.MeasurementHandlers;
|
|
||||||
|
|
||||||
const MEASUREMENT_ACTION_MAP = {
|
|
||||||
added: onAdded,
|
|
||||||
removed: onRemoved,
|
|
||||||
modified: throttle(event => {
|
|
||||||
return onModified(event);
|
|
||||||
}, 300),
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* @export
|
|
||||||
* @param {Object} servicesManager
|
|
||||||
* @param {Object} configuration
|
|
||||||
*/
|
|
||||||
export default function init({
|
|
||||||
servicesManager,
|
|
||||||
commandsManager,
|
|
||||||
configuration,
|
|
||||||
}) {
|
|
||||||
const { UIDialogService } = servicesManager.services;
|
|
||||||
|
|
||||||
// TODO: MEASUREMENT_COMPLETED (not present in initial implementation)
|
|
||||||
const onMeasurementsChanged = (action, event) => {
|
|
||||||
return MEASUREMENT_ACTION_MAP[action](event);
|
|
||||||
};
|
|
||||||
const onMeasurementAdded = onMeasurementsChanged.bind(this, 'added');
|
|
||||||
const onMeasurementRemoved = onMeasurementsChanged.bind(this, 'removed');
|
|
||||||
const onMeasurementModified = onMeasurementsChanged.bind(this, 'modified');
|
|
||||||
const onLabelmapModified = onMeasurementsChanged.bind(
|
|
||||||
this,
|
|
||||||
'labelmapModified'
|
|
||||||
);
|
|
||||||
|
|
||||||
const _getDefaultPosition = event => ({
|
|
||||||
x: (event && event.currentPoints.client.x) || 0,
|
|
||||||
y: (event && event.currentPoints.client.y) || 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
const _updateLabellingHandler = (labellingData, measurementData) => {
|
|
||||||
const { location, description, response } = labellingData;
|
|
||||||
|
|
||||||
if (location) {
|
|
||||||
measurementData.location = location;
|
|
||||||
}
|
|
||||||
|
|
||||||
measurementData.description = description || '';
|
|
||||||
|
|
||||||
if (response) {
|
|
||||||
measurementData.response = response;
|
|
||||||
}
|
|
||||||
|
|
||||||
commandsManager.runCommand(
|
|
||||||
'updateTableWithNewMeasurementData',
|
|
||||||
measurementData
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const showLabellingDialog = (props, contentProps, measurementData) => {
|
|
||||||
if (!UIDialogService) {
|
|
||||||
console.warn('Unable to show dialog; no UI Dialog Service available.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
UIDialogService.create({
|
|
||||||
id: 'labelling',
|
|
||||||
isDraggable: false,
|
|
||||||
showOverlay: true,
|
|
||||||
centralize: true,
|
|
||||||
content: LabellingFlow,
|
|
||||||
contentProps: {
|
|
||||||
measurementData,
|
|
||||||
labellingDoneCallback: () =>
|
|
||||||
UIDialogService.dismiss({ id: 'labelling' }),
|
|
||||||
updateLabelling: labellingData =>
|
|
||||||
_updateLabellingHandler(labellingData, measurementData),
|
|
||||||
...contentProps,
|
|
||||||
},
|
|
||||||
...props,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const onRightClick = event => {
|
|
||||||
if (!UIDialogService) {
|
|
||||||
console.warn('Unable to show dialog; no UI Dialog Service available.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
UIDialogService.dismiss({ id: 'context-menu' });
|
|
||||||
UIDialogService.create({
|
|
||||||
id: 'context-menu',
|
|
||||||
isDraggable: false,
|
|
||||||
preservePosition: false,
|
|
||||||
defaultPosition: _getDefaultPosition(event.detail),
|
|
||||||
content: ToolContextMenu,
|
|
||||||
contentProps: {
|
|
||||||
eventData: event.detail,
|
|
||||||
onDelete: (nearbyToolData, eventData) => {
|
|
||||||
const element = eventData.element;
|
|
||||||
commandsManager.runCommand('removeToolState', {
|
|
||||||
element,
|
|
||||||
toolType: nearbyToolData.toolType,
|
|
||||||
tool: nearbyToolData.tool,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onClose: () => UIDialogService.dismiss({ id: 'context-menu' }),
|
|
||||||
onSetLabel: (eventData, measurementData) => {
|
|
||||||
showLabellingDialog(
|
|
||||||
{ centralize: true, isDraggable: false },
|
|
||||||
{ skipAddLabelButton: true, editLocation: true },
|
|
||||||
measurementData
|
|
||||||
);
|
|
||||||
},
|
|
||||||
onSetDescription: (eventData, measurementData) => {
|
|
||||||
showLabellingDialog(
|
|
||||||
{ defaultPosition: _getDefaultPosition(eventData) },
|
|
||||||
{ editDescriptionOnDialog: true },
|
|
||||||
measurementData
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const onTouchPress = event => {
|
|
||||||
if (!UIDialogService) {
|
|
||||||
console.warn('Unable to show dialog; no UI Dialog Service available.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
UIDialogService.create({
|
|
||||||
eventData: event.detail,
|
|
||||||
content: ToolContextMenu,
|
|
||||||
contentProps: {
|
|
||||||
isTouchEvent: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const onTouchStart = () => resetLabelligAndContextMenu();
|
|
||||||
|
|
||||||
const onMouseClick = () => resetLabelligAndContextMenu();
|
|
||||||
|
|
||||||
const resetLabelligAndContextMenu = () => {
|
|
||||||
if (!UIDialogService) {
|
|
||||||
console.warn('Unable to show dialog; no UI Dialog Service available.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
UIDialogService.dismiss({ id: 'context-menu' });
|
|
||||||
UIDialogService.dismiss({ id: 'labelling' });
|
|
||||||
};
|
|
||||||
|
|
||||||
// TODO: This makes scrolling painfully slow
|
|
||||||
// const onNewImage = ...
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Because click gives us the native "mouse up", buttons will always be `0`
|
|
||||||
* Need to fallback to event.which;
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
const handleClick = cornerstoneMouseClickEvent => {
|
|
||||||
const mouseUpEvent = cornerstoneMouseClickEvent.detail.event;
|
|
||||||
const isRightClick = mouseUpEvent.which === 3;
|
|
||||||
|
|
||||||
if (isRightClick) {
|
|
||||||
onRightClick(cornerstoneMouseClickEvent);
|
|
||||||
} else {
|
|
||||||
onMouseClick(cornerstoneMouseClickEvent);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
function elementEnabledHandler(evt) {
|
|
||||||
const element = evt.detail.element;
|
|
||||||
|
|
||||||
element.addEventListener(
|
|
||||||
csTools.EVENTS.MEASUREMENT_ADDED,
|
|
||||||
onMeasurementAdded
|
|
||||||
);
|
|
||||||
element.addEventListener(
|
|
||||||
csTools.EVENTS.MEASUREMENT_REMOVED,
|
|
||||||
onMeasurementRemoved
|
|
||||||
);
|
|
||||||
element.addEventListener(
|
|
||||||
csTools.EVENTS.MEASUREMENT_MODIFIED,
|
|
||||||
onMeasurementModified
|
|
||||||
);
|
|
||||||
element.addEventListener(
|
|
||||||
csTools.EVENTS.LABELMAP_MODIFIED,
|
|
||||||
onLabelmapModified
|
|
||||||
);
|
|
||||||
|
|
||||||
element.addEventListener(csTools.EVENTS.TOUCH_PRESS, onTouchPress);
|
|
||||||
element.addEventListener(csTools.EVENTS.MOUSE_CLICK, handleClick);
|
|
||||||
element.addEventListener(csTools.EVENTS.TOUCH_START, onTouchStart);
|
|
||||||
|
|
||||||
// TODO: This makes scrolling painfully slow
|
|
||||||
// element.addEventListener(cornerstone.EVENTS.NEW_IMAGE, onNewImage);
|
|
||||||
}
|
|
||||||
|
|
||||||
function elementDisabledHandler(evt) {
|
|
||||||
const element = evt.detail.element;
|
|
||||||
|
|
||||||
element.removeEventListener(
|
|
||||||
csTools.EVENTS.MEASUREMENT_ADDED,
|
|
||||||
onMeasurementAdded
|
|
||||||
);
|
|
||||||
element.removeEventListener(
|
|
||||||
csTools.EVENTS.MEASUREMENT_REMOVED,
|
|
||||||
onMeasurementRemoved
|
|
||||||
);
|
|
||||||
element.removeEventListener(
|
|
||||||
csTools.EVENTS.MEASUREMENT_MODIFIED,
|
|
||||||
onMeasurementModified
|
|
||||||
);
|
|
||||||
element.removeEventListener(
|
|
||||||
csTools.EVENTS.LABELMAP_MODIFIED,
|
|
||||||
onLabelmapModified
|
|
||||||
);
|
|
||||||
|
|
||||||
element.removeEventListener(csTools.EVENTS.TOUCH_PRESS, onTouchPress);
|
|
||||||
element.removeEventListener(csTools.EVENTS.MOUSE_CLICK, handleClick);
|
|
||||||
element.removeEventListener(csTools.EVENTS.TOUCH_START, onTouchStart);
|
|
||||||
|
|
||||||
// TODO: This makes scrolling painfully slow
|
|
||||||
// element.removeEventListener(cornerstone.EVENTS.NEW_IMAGE, onNewImage);
|
|
||||||
}
|
|
||||||
|
|
||||||
cornerstone.events.addEventListener(
|
|
||||||
cornerstone.EVENTS.ELEMENT_ENABLED,
|
|
||||||
elementEnabledHandler
|
|
||||||
);
|
|
||||||
cornerstone.events.addEventListener(
|
|
||||||
cornerstone.EVENTS.ELEMENT_DISABLED,
|
|
||||||
elementDisabledHandler
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,4 +0,0 @@
|
|||||||
import GenericViewerCommands from './GenericViewerCommands/index.js';
|
|
||||||
import MeasurementsPanel from './MeasurementsPanel/index.js';
|
|
||||||
|
|
||||||
export { GenericViewerCommands, MeasurementsPanel };
|
|
||||||
0
platform/viewer/src/components/.gitkeep
Normal file
0
platform/viewer/src/components/.gitkeep
Normal file
@ -1,37 +0,0 @@
|
|||||||
/**
|
|
||||||
* We use this component to leverage "Code Splitting"
|
|
||||||
*
|
|
||||||
* Link: https://serverless-stack.com/chapters/code-splitting-in-create-react-app.html
|
|
||||||
*/
|
|
||||||
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
|
|
||||||
export default function asyncComponent(importComponent) {
|
|
||||||
class AsyncComponent extends Component {
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
component: null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async componentDidMount() {
|
|
||||||
// Add dynamically loaded component to state
|
|
||||||
const { default: component } = await importComponent();
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
component: component,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const C = this.state.component;
|
|
||||||
|
|
||||||
// Render the loaded component, or null
|
|
||||||
return C ? <C {...this.props} /> : null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return AsyncComponent;
|
|
||||||
}
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
.editDescriptionDialog {
|
|
||||||
position: relative;
|
|
||||||
z-index: 300;
|
|
||||||
width: 320px;
|
|
||||||
transition: all 300ms linear;
|
|
||||||
}
|
|
||||||
@ -1,64 +0,0 @@
|
|||||||
import { Component } from 'react';
|
|
||||||
import React from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import SimpleDialog from '../SimpleDialog/SimpleDialog.js';
|
|
||||||
import './EditDescriptionDialog.css';
|
|
||||||
|
|
||||||
export default class EditDescriptionDialog extends Component {
|
|
||||||
static propTypes = {
|
|
||||||
description: PropTypes.string,
|
|
||||||
measurementData: PropTypes.object.isRequired,
|
|
||||||
onCancel: PropTypes.func.isRequired,
|
|
||||||
onUpdate: PropTypes.func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
description: props.measurementData.description || '',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
|
||||||
if (this.props.description !== prevProps.description) {
|
|
||||||
this.setState({
|
|
||||||
description: this.props.description,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<SimpleDialog
|
|
||||||
headerTitle="Edit Description"
|
|
||||||
onClose={this.onClose}
|
|
||||||
onConfirm={this.onConfirm}
|
|
||||||
rootClass="editDescriptionDialog"
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
value={this.state.description}
|
|
||||||
className="simpleDialogInput"
|
|
||||||
id="description"
|
|
||||||
autoComplete="off"
|
|
||||||
autoFocus
|
|
||||||
onChange={this.handleChange}
|
|
||||||
/>
|
|
||||||
</SimpleDialog>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
onClose = () => {
|
|
||||||
this.props.onCancel();
|
|
||||||
};
|
|
||||||
|
|
||||||
onConfirm = e => {
|
|
||||||
e.preventDefault();
|
|
||||||
this.props.onUpdate(this.state.description);
|
|
||||||
};
|
|
||||||
|
|
||||||
handleChange = event => {
|
|
||||||
this.setState({ description: event.target.value });
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
import Header from './Header';
|
|
||||||
|
|
||||||
export default Header;
|
|
||||||
@ -1,126 +0,0 @@
|
|||||||
.labellingComponent {
|
|
||||||
position: relative;
|
|
||||||
text-align: center;
|
|
||||||
z-index: 999;
|
|
||||||
transition: all 200ms linear;
|
|
||||||
max-height: 500px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.labellingComponent .selectedLabel, .labellingComponent .selectedDescription {
|
|
||||||
padding: 5px;
|
|
||||||
background-color: white;
|
|
||||||
width: 150px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.labellingComponent .addLabelButton {
|
|
||||||
color: #000000;
|
|
||||||
background-color: #20a5d6;
|
|
||||||
border: 2px solid #44626f;
|
|
||||||
border-radius: 14px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-weight: bold;
|
|
||||||
font-size: 13px;
|
|
||||||
line-height: 24px;
|
|
||||||
opacity: 1;
|
|
||||||
padding: 0 14px;
|
|
||||||
transition: opacity 0.3s ease;
|
|
||||||
outline: none;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.labellingComponent .commonButton {
|
|
||||||
border: 1px solid #44626f;
|
|
||||||
color: #ffffff;
|
|
||||||
background-color: #000000;
|
|
||||||
border-radius: 16px;
|
|
||||||
font-weight: bold;
|
|
||||||
font-size: 13px;
|
|
||||||
line-height: 26px;
|
|
||||||
padding: 0 12px;
|
|
||||||
margin: 10px 5px 0 0;
|
|
||||||
outline: none;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.labellingComponent .locationDescriptionWrapper {
|
|
||||||
background-color: #ffffff;
|
|
||||||
line-height: 46px;
|
|
||||||
height: 46px;
|
|
||||||
font-size: 13px;
|
|
||||||
position: relative;
|
|
||||||
width: 290px;
|
|
||||||
min-width: 260px;
|
|
||||||
padding: 0 12px;
|
|
||||||
margin: 0 auto;
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.labellingComponent .locationDescriptionWrapper .location {
|
|
||||||
transition: all 300ms linear;
|
|
||||||
position: absolute;
|
|
||||||
white-space: nowrap;
|
|
||||||
bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.labellingComponent.editDescription .locationDescriptionWrapper .location {
|
|
||||||
bottom: 38px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.labellingComponent .locationDescriptionWrapper #descriptionInput {
|
|
||||||
transition-delay: all 300ms linear;
|
|
||||||
visibility: hidden;
|
|
||||||
outline: none;
|
|
||||||
height: 46px;
|
|
||||||
width: 100%;
|
|
||||||
line-height: 20px;
|
|
||||||
font-size: 13px;
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.labellingComponent.editDescription .locationDescriptionWrapper #descriptionInput {
|
|
||||||
visibility: visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
.labellingComponent.editDescription .location {
|
|
||||||
color: #337ab7;
|
|
||||||
}
|
|
||||||
|
|
||||||
.labellingComponent .commonButtons, .labellingComponent.editDescription .editDescriptionButtons {
|
|
||||||
display: block;
|
|
||||||
margin-left: 55px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.labellingComponent.editDescription .commonButtons, .labellingComponent .editDescriptionButtons {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.labellingComponent .commonButtons {
|
|
||||||
text-align: center;
|
|
||||||
margin-left: 55px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.labellingComponent .commonButton.left {
|
|
||||||
float: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.labellingComponent .commonButton.right {
|
|
||||||
float: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
.labellingComponent .checkIconWrapper {
|
|
||||||
display: inline-block;
|
|
||||||
background-color: #337ab7;
|
|
||||||
border-radius: 46px;
|
|
||||||
width: 46px;
|
|
||||||
height: 46px;
|
|
||||||
margin-right: 10px;
|
|
||||||
vertical-align: bottom;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.labellingComponent .checkIcon {
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
margin: 13px;
|
|
||||||
fill: black;
|
|
||||||
}
|
|
||||||
@ -1,263 +0,0 @@
|
|||||||
import { Icon, SelectTree } from '@ohif/ui';
|
|
||||||
import React, { useState, useEffect, useRef } from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import cloneDeep from 'lodash.clonedeep';
|
|
||||||
|
|
||||||
import LabellingTransition from './LabellingTransition.js';
|
|
||||||
import OHIFLabellingData from './OHIFLabellingData.js';
|
|
||||||
import EditDescriptionDialog from './../EditDescriptionDialog/EditDescriptionDialog.js';
|
|
||||||
import './LabellingFlow.css';
|
|
||||||
|
|
||||||
const LabellingFlow = ({
|
|
||||||
measurementData,
|
|
||||||
editLocation,
|
|
||||||
editDescription,
|
|
||||||
skipAddLabelButton,
|
|
||||||
updateLabelling,
|
|
||||||
labellingDoneCallback,
|
|
||||||
editDescriptionOnDialog,
|
|
||||||
}) => {
|
|
||||||
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 }));
|
|
||||||
};
|
|
||||||
|
|
||||||
const descriptionCancel = () => {
|
|
||||||
const { description = '' } = cloneDeep(state);
|
|
||||||
descriptionInput.current.value = description;
|
|
||||||
setState(state => ({ ...state, editDescription: false }));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleKeyPress = event => {
|
|
||||||
if (event.key === 'Enter') {
|
|
||||||
descriptionSave();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const descriptionSave = () => {
|
|
||||||
const description = descriptionInput.current.value;
|
|
||||||
updateLabelling({ description });
|
|
||||||
|
|
||||||
setState(state => ({
|
|
||||||
...state,
|
|
||||||
description,
|
|
||||||
editDescription: false,
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
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) {
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="addLabelButton"
|
|
||||||
onClick={showLabelling}
|
|
||||||
>
|
|
||||||
{location ? 'Edit' : 'Add'} Label
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
if (editLocation) {
|
|
||||||
return (
|
|
||||||
<SelectTree
|
|
||||||
items={OHIFLabellingData}
|
|
||||||
columns={1}
|
|
||||||
onSelected={selectTreeSelectCallback}
|
|
||||||
selectTreeFirstTitle="Assign Label"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="checkIconWrapper" onClick={fadeOutAndLeaveFast}>
|
|
||||||
<Icon name="check" className="checkIcon" />
|
|
||||||
</div>
|
|
||||||
<div className="locationDescriptionWrapper">
|
|
||||||
<div className="location">{locationLabel}</div>
|
|
||||||
<div className="description">
|
|
||||||
<input
|
|
||||||
id="descriptionInput"
|
|
||||||
ref={descriptionInput}
|
|
||||||
defaultValue={description || ''}
|
|
||||||
autoComplete="off"
|
|
||||||
onKeyPress={handleKeyPress}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="commonButtons">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="commonButton left"
|
|
||||||
onClick={relabel}
|
|
||||||
>
|
|
||||||
Relabel
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="commonButton right"
|
|
||||||
onClick={setDescriptionUpdateMode}
|
|
||||||
>
|
|
||||||
{description ? 'Edit ' : 'Add '}
|
|
||||||
Description
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className="editDescriptionButtons">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="commonButton left"
|
|
||||||
onClick={descriptionCancel}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="commonButton right"
|
|
||||||
onClick={descriptionSave}
|
|
||||||
>
|
|
||||||
Save
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (editDescriptionOnDialog) {
|
|
||||||
return (
|
|
||||||
<EditDescriptionDialog
|
|
||||||
onCancel={labellingDoneCallback}
|
|
||||||
onUpdate={descriptionDialogUpdate}
|
|
||||||
measurementData={state.measurementData}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<LabellingTransition
|
|
||||||
displayComponent={showComponent}
|
|
||||||
onTransitionExit={labellingDoneCallback}
|
|
||||||
>
|
|
||||||
<>
|
|
||||||
<div
|
|
||||||
className={`labellingComponent ${state.editDescription &&
|
|
||||||
'editDescription'}`}
|
|
||||||
onMouseLeave={fadeOutAndLeave}
|
|
||||||
onMouseEnter={clearFadeOutTimer}
|
|
||||||
>
|
|
||||||
{labellingStateFragment()}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
</LabellingTransition>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
LabellingFlow.propTypes = {
|
|
||||||
measurementData: PropTypes.object.isRequired,
|
|
||||||
labellingDoneCallback: PropTypes.func.isRequired,
|
|
||||||
updateLabelling: PropTypes.func.isRequired,
|
|
||||||
initialTopDistance: PropTypes.number,
|
|
||||||
skipAddLabelButton: PropTypes.bool,
|
|
||||||
editLocation: PropTypes.bool,
|
|
||||||
editDescription: PropTypes.bool,
|
|
||||||
editDescriptionOnDialog: PropTypes.bool,
|
|
||||||
};
|
|
||||||
|
|
||||||
LabellingFlow.defaultProps = {
|
|
||||||
skipAddLabelButton: false,
|
|
||||||
editLocation: false,
|
|
||||||
editDescription: false,
|
|
||||||
editDescriptionOnDialog: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default LabellingFlow;
|
|
||||||
@ -1,21 +0,0 @@
|
|||||||
.labelling-appear {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.labelling-appear.labelling-appear-active {
|
|
||||||
opacity: 1;
|
|
||||||
transition: opacity 500ms linear;
|
|
||||||
}
|
|
||||||
|
|
||||||
.labelling-exit {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.labelling-exit.labelling-exit-active {
|
|
||||||
opacity: 0;
|
|
||||||
transition: opacity 500ms linear;
|
|
||||||
}
|
|
||||||
|
|
||||||
.labelling-exit-done {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
@ -1,31 +0,0 @@
|
|||||||
import React, { Component } from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { CSSTransition } from 'react-transition-group';
|
|
||||||
|
|
||||||
import './LabellingTransition.css';
|
|
||||||
|
|
||||||
// If these variables changes, CSS must be updated
|
|
||||||
const transitionDuration = 500;
|
|
||||||
const transitionClassName = 'labelling';
|
|
||||||
const transitionOnAppear = true;
|
|
||||||
|
|
||||||
export default class LabellingTransition extends Component {
|
|
||||||
static propTypes = {
|
|
||||||
children: PropTypes.node.isRequired,
|
|
||||||
displayComponent: PropTypes.bool.isRequired,
|
|
||||||
onTransitionExit: PropTypes.func.isRequired,
|
|
||||||
};
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<CSSTransition
|
|
||||||
in={this.props.displayComponent}
|
|
||||||
appear={transitionOnAppear}
|
|
||||||
timeout={transitionDuration}
|
|
||||||
classNames={transitionClassName}
|
|
||||||
onExited={this.props.onTransitionExit}
|
|
||||||
>
|
|
||||||
{this.props.children}
|
|
||||||
</CSSTransition>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,39 +0,0 @@
|
|||||||
const items = [
|
|
||||||
'Abdomen/Chest Wall',
|
|
||||||
'Adrenal',
|
|
||||||
'Bladder',
|
|
||||||
'Bone',
|
|
||||||
'Brain',
|
|
||||||
'Breast',
|
|
||||||
'Colon',
|
|
||||||
'Esophagus',
|
|
||||||
'Extremities',
|
|
||||||
'Gallbladder',
|
|
||||||
'Kidney',
|
|
||||||
'Liver',
|
|
||||||
'Lung',
|
|
||||||
'Lymph Node',
|
|
||||||
'Mediastinum/Hilum',
|
|
||||||
'Muscle',
|
|
||||||
'Neck',
|
|
||||||
'Other Soft Tissue',
|
|
||||||
'Ovary',
|
|
||||||
'Pancreas',
|
|
||||||
'Pelvis',
|
|
||||||
'Peritoneum/Omentum',
|
|
||||||
'Prostate',
|
|
||||||
'Retroperitoneum',
|
|
||||||
'Small Bowel',
|
|
||||||
'Spleen',
|
|
||||||
'Stomach',
|
|
||||||
'Subcutaneous',
|
|
||||||
];
|
|
||||||
|
|
||||||
const OHIFLabellingData = items.map(item => {
|
|
||||||
return {
|
|
||||||
label: item,
|
|
||||||
value: item,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
export default OHIFLabellingData;
|
|
||||||
@ -1,32 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
const Bar = ({ progress, animationDuration }) => (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
background: '#29d',
|
|
||||||
height: 2,
|
|
||||||
left: 0,
|
|
||||||
marginLeft: `${(-1 + progress) * 100}%`,
|
|
||||||
position: 'fixed',
|
|
||||||
top: 0,
|
|
||||||
transition: `margin-left ${animationDuration}ms linear`,
|
|
||||||
width: '100%',
|
|
||||||
zIndex: 1031,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
boxShadow: '0 0 10px #29d, 0 0 5px #29d',
|
|
||||||
display: 'block',
|
|
||||||
height: '100%',
|
|
||||||
opacity: 1,
|
|
||||||
position: 'absolute',
|
|
||||||
right: 0,
|
|
||||||
transform: 'rotate(3deg) translate(0px, -4px)',
|
|
||||||
width: 100,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
export default Bar;
|
|
||||||
@ -1,15 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
const Container = ({ children, isFinished, animationDuration }) => (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
opacity: isFinished ? 0 : 1,
|
|
||||||
pointerEvents: 'none',
|
|
||||||
transition: `opacity ${animationDuration}ms linear`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
export default Container;
|
|
||||||
@ -1,4 +0,0 @@
|
|||||||
import Bar from './Bar.js';
|
|
||||||
import Container from './Container.js';
|
|
||||||
|
|
||||||
export { Bar, Container };
|
|
||||||
@ -1,33 +0,0 @@
|
|||||||
.header-brand {
|
|
||||||
height: 30px;
|
|
||||||
text-decoration: none;
|
|
||||||
text-decoration-color: none;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
color: var(--text-primary-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-brand:hover,
|
|
||||||
.header-brand:active,
|
|
||||||
.header-band:visited {
|
|
||||||
color: var(--text-primary-color);
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-logo-text {
|
|
||||||
width: 224px;
|
|
||||||
height: auto;
|
|
||||||
margin-left: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-logo-image {
|
|
||||||
height: 30px;
|
|
||||||
width: 30px;
|
|
||||||
font-size: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media only screen and (max-width: 768px) {
|
|
||||||
.header-logo-text {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,27 +0,0 @@
|
|||||||
import './OHIFLogo.css';
|
|
||||||
|
|
||||||
import { Icon } from '@ohif/ui';
|
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
function OHIFLogo() {
|
|
||||||
return (
|
|
||||||
<a
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="header-brand"
|
|
||||||
href="http://ohif.org"
|
|
||||||
>
|
|
||||||
<Icon name="ohif-logo" className="header-logo-image" />
|
|
||||||
{/* Logo text would fit smaller displays at two lines:
|
|
||||||
*
|
|
||||||
* Open Health
|
|
||||||
* Imaging Foundation
|
|
||||||
*
|
|
||||||
* Or as `OHIF` on really small displays
|
|
||||||
*/}
|
|
||||||
<Icon name="ohif-text-logo" className="header-logo-text" />
|
|
||||||
</a>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default OHIFLogo;
|
|
||||||
@ -1,41 +0,0 @@
|
|||||||
import './SidePanel.css';
|
|
||||||
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import classNames from 'classnames';
|
|
||||||
|
|
||||||
class SidePanel extends Component {
|
|
||||||
static propTypes = {
|
|
||||||
from: PropTypes.string.isRequired,
|
|
||||||
isOpen: PropTypes.bool.isRequired,
|
|
||||||
children: PropTypes.node,
|
|
||||||
width: PropTypes.string,
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const fromSideClass =
|
|
||||||
this.props.from === 'right' ? 'from-right' : 'from-left';
|
|
||||||
|
|
||||||
const styles = this.props.width
|
|
||||||
? {
|
|
||||||
maxWidth: this.props.width,
|
|
||||||
marginRight: this.props.isOpen
|
|
||||||
? '0'
|
|
||||||
: Number.parseInt(this.props.width) * -1,
|
|
||||||
}
|
|
||||||
: {};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<section
|
|
||||||
style={styles}
|
|
||||||
className={classNames('sidepanel', fromSideClass, {
|
|
||||||
'is-open': this.props.isOpen,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{this.props.children}
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default SidePanel;
|
|
||||||
@ -1,133 +0,0 @@
|
|||||||
.simpleDialog {
|
|
||||||
position: fixed;
|
|
||||||
border: 0;
|
|
||||||
border-radius: 6px;
|
|
||||||
background-color: #151a1f;
|
|
||||||
}
|
|
||||||
.simpleDialog .header {
|
|
||||||
border-bottom-width: 3px;
|
|
||||||
border-bottom-style: solid;
|
|
||||||
border-bottom-color: #000;
|
|
||||||
padding: 19px 22px 17px;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.simpleDialog .header .title {
|
|
||||||
font-size: 20px;
|
|
||||||
font-weight: 600;
|
|
||||||
line-height: 24px;
|
|
||||||
padding-right: 40px;
|
|
||||||
color: #91b9cd;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.simpleDialog .header .closeBtn {
|
|
||||||
height: 20px;
|
|
||||||
opacity: 1;
|
|
||||||
overflow: hidden;
|
|
||||||
padding: 2px;
|
|
||||||
text-align: center;
|
|
||||||
text-shadow: none;
|
|
||||||
width: 20px;
|
|
||||||
color: #91b9cd;
|
|
||||||
cursor: pointer;
|
|
||||||
position: absolute;
|
|
||||||
right: 21px;
|
|
||||||
top: 50%;
|
|
||||||
-webkit-transform: translateY(-50%);
|
|
||||||
transform: translateY(-50%);
|
|
||||||
transition: color 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.simpleDialog .header .closeIcon {
|
|
||||||
color: transparent;
|
|
||||||
display: block;
|
|
||||||
font-size: 0;
|
|
||||||
height: 100%;
|
|
||||||
line-height: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
position: relative;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.simpleDialog .header .closeIcon:after,
|
|
||||||
.simpleDialog .header .closeIcon:before {
|
|
||||||
content: ' ';
|
|
||||||
display: block;
|
|
||||||
height: 2px;
|
|
||||||
transition: background-color 0.3s ease;
|
|
||||||
width: 19px;
|
|
||||||
background-color: #91b9cd;
|
|
||||||
}
|
|
||||||
|
|
||||||
.simpleDialog .header .closeIcon:before {
|
|
||||||
left: 1px;
|
|
||||||
position: absolute;
|
|
||||||
top: 1px;
|
|
||||||
-webkit-transform: rotate(45deg);
|
|
||||||
transform: rotate(45deg);
|
|
||||||
-webkit-transform-origin: 1px 50%;
|
|
||||||
transform-origin: 1px 50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.simpleDialog .header .closeIcon:after {
|
|
||||||
right: 1px;
|
|
||||||
position: absolute;
|
|
||||||
top: 1px;
|
|
||||||
-webkit-transform: rotate(-45deg);
|
|
||||||
transform: rotate(-45deg);
|
|
||||||
-webkit-transform-origin: calc(100% - 1px) 50%;
|
|
||||||
transform-origin: calc(100% - 1px) 50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.simpleDialog .content {
|
|
||||||
padding: 16px 22px 25px;
|
|
||||||
position: relative;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.simpleDialog .content .simpleDialogSelect,
|
|
||||||
.simpleDialog .content .simpleDialogInput {
|
|
||||||
background-color: #fff;
|
|
||||||
color: #000;
|
|
||||||
border: 0;
|
|
||||||
border-radius: 2px;
|
|
||||||
font-size: 14px;
|
|
||||||
height: 30px;
|
|
||||||
width: 100%;
|
|
||||||
line-height: 16px;
|
|
||||||
padding: 8px 9px 6px;
|
|
||||||
margin-top: 10px;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.simpleDialog .content .simpleDialogInputLabel {
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 700;
|
|
||||||
line-height: 16px;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.simpleDialog .footer {
|
|
||||||
padding: 15px;
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
.simpleDialog .footer .btn {
|
|
||||||
transition: background-color 0.3s ease;
|
|
||||||
color: #000;
|
|
||||||
border: 0;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 15px;
|
|
||||||
font-weight: 400;
|
|
||||||
height: 37px;
|
|
||||||
line-height: 37px;
|
|
||||||
padding: 0 12px;
|
|
||||||
margin-bottom: 0;
|
|
||||||
margin-left: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.simpleDialog .footer .btn-confirm {
|
|
||||||
color: #fff;
|
|
||||||
background-color: #337ab7;
|
|
||||||
}
|
|
||||||
@ -1,56 +0,0 @@
|
|||||||
import { Component } from 'react';
|
|
||||||
import React from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import './SimpleDialog.css';
|
|
||||||
|
|
||||||
class SimpleDialog extends Component {
|
|
||||||
static defaultProps = {
|
|
||||||
componentStyle: {},
|
|
||||||
rootClass: '',
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={`simpleDialog ${this.props.rootClass}`}
|
|
||||||
ref={this.props.componentRef}
|
|
||||||
style={this.props.componentStyle}
|
|
||||||
>
|
|
||||||
<form onSubmit={this.props.onConfirm}>
|
|
||||||
<div className="header">
|
|
||||||
<span className="closeBtn" onClick={this.props.onClose}>
|
|
||||||
<span className="closeIcon">x</span>
|
|
||||||
</span>
|
|
||||||
<h4 className="title">{this.props.headerTitle}</h4>
|
|
||||||
</div>
|
|
||||||
<div className="content">{this.props.children}</div>
|
|
||||||
<div className="footer">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn btn-cancel"
|
|
||||||
onClick={this.props.onClose}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn btn-confirm"
|
|
||||||
onClick={this.props.onConfirm}
|
|
||||||
>
|
|
||||||
Confirm
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
SimpleDialog.propTypes = {
|
|
||||||
headerTitle: PropTypes.string.isRequired,
|
|
||||||
onClose: PropTypes.func.isRequired,
|
|
||||||
onConfirm: PropTypes.func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SimpleDialog;
|
|
||||||
@ -1,42 +0,0 @@
|
|||||||
import { Component } from "react";
|
|
||||||
import PropTypes from "prop-types";
|
|
||||||
import OHIF from "@ohif/core";
|
|
||||||
|
|
||||||
class StudyLoadingMonitor extends Component {
|
|
||||||
static propTypes = {
|
|
||||||
studies: PropTypes.array.isRequired,
|
|
||||||
setStudyLoadingProgress: PropTypes.func.isRequired,
|
|
||||||
clearStudyLoadingProgress: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
// TODO: This is pretty ugly. The thing is that the StudyLoadingListener
|
|
||||||
// needs to update the Redux store, but shouldn't know that it exists.
|
|
||||||
// I am therefore passing in some functions to update the store instead,
|
|
||||||
// but this should definitely be cleaned up somehow.
|
|
||||||
const options = {
|
|
||||||
_setProgressData: (progressId, progressData) => {
|
|
||||||
this.props.setStudyLoadingProgress(progressId, progressData);
|
|
||||||
},
|
|
||||||
_clearProgressById: progressId => {
|
|
||||||
this.props.clearStudyLoadingProgress(progressId);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const { StudyLoadingListener } = OHIF.classes;
|
|
||||||
this.studyLoadingListener = StudyLoadingListener.getInstance(options);
|
|
||||||
this.studyLoadingListener.clear();
|
|
||||||
this.studyLoadingListener.addStudies(this.props.studies);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
// Destroy stack loading listeners when we close the viewer
|
|
||||||
this.studyLoadingListener.clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default StudyLoadingMonitor;
|
|
||||||
@ -1,29 +0,0 @@
|
|||||||
import { Component } from "react";
|
|
||||||
import PropTypes from "prop-types";
|
|
||||||
import OHIF from "@ohif/core";
|
|
||||||
|
|
||||||
const { StudyPrefetcher } = OHIF.classes;
|
|
||||||
|
|
||||||
class StudyPrefetcherComponent extends Component {
|
|
||||||
static propTypes = {
|
|
||||||
studies: PropTypes.array
|
|
||||||
};
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
const { studies } = this.props;
|
|
||||||
|
|
||||||
this.studyPrefetcher = StudyPrefetcher.getInstance();
|
|
||||||
this.studyPrefetcher.setStudies(studies);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
// Stop prefetching when we close the viewer
|
|
||||||
this.studyPrefetcher.destroy();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default StudyPrefetcherComponent;
|
|
||||||
@ -1,72 +0,0 @@
|
|||||||
import React, { useState, useSelector } from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import i18n from '@ohif/i18n';
|
|
||||||
|
|
||||||
import { TabFooter, LanguageSwitcher, useSnackbarContext } from '@ohif/ui';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
|
|
||||||
import './GeneralPreferences.styl';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* General Preferences tab
|
|
||||||
* It renders the General Preferences content
|
|
||||||
*
|
|
||||||
* @param {object} props component props
|
|
||||||
* @param {function} props.onClose
|
|
||||||
*/
|
|
||||||
function GeneralPreferences({ onClose }) {
|
|
||||||
const { t } = useTranslation('UserPreferencesModal');
|
|
||||||
const snackbar = useSnackbarContext();
|
|
||||||
const currentLanguage = i18n.language;
|
|
||||||
const { availableLanguages } = i18n;
|
|
||||||
|
|
||||||
const [language, setLanguage] = useState(currentLanguage);
|
|
||||||
|
|
||||||
const onResetPreferences = () => {
|
|
||||||
setLanguage(i18n.defaultLanguage);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onSave = () => {
|
|
||||||
i18n.changeLanguage(language);
|
|
||||||
|
|
||||||
onClose();
|
|
||||||
|
|
||||||
snackbar.show({
|
|
||||||
message: t('SaveMessage'),
|
|
||||||
type: 'success',
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const hasErrors = false;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<React.Fragment>
|
|
||||||
<div className="GeneralPreferences">
|
|
||||||
<div className="language">
|
|
||||||
<label htmlFor="language-select" className="languageLabel">
|
|
||||||
Language
|
|
||||||
</label>
|
|
||||||
<LanguageSwitcher
|
|
||||||
language={language}
|
|
||||||
onLanguageChange={setLanguage}
|
|
||||||
languages={availableLanguages}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<TabFooter
|
|
||||||
onResetPreferences={onResetPreferences}
|
|
||||||
onSave={onSave}
|
|
||||||
onCancel={onClose}
|
|
||||||
hasErrors={hasErrors}
|
|
||||||
t={t}
|
|
||||||
/>
|
|
||||||
</React.Fragment>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
GeneralPreferences.propTypes = {
|
|
||||||
onClose: PropTypes.func,
|
|
||||||
};
|
|
||||||
|
|
||||||
export { GeneralPreferences };
|
|
||||||
@ -1,18 +0,0 @@
|
|||||||
.GeneralPreferences
|
|
||||||
display: flex;
|
|
||||||
padding: 20px 0;
|
|
||||||
|
|
||||||
.language
|
|
||||||
display: flex;
|
|
||||||
width: 50%;
|
|
||||||
|
|
||||||
.languageLabel
|
|
||||||
display: flex;
|
|
||||||
flex-basis: 0;
|
|
||||||
flex-grow: 1.5;
|
|
||||||
justify-content: flex-end;
|
|
||||||
padding-right: 20px;
|
|
||||||
|
|
||||||
.language-select
|
|
||||||
flex-grow: 1.5;
|
|
||||||
flex-basis: 0;
|
|
||||||
@ -1,203 +0,0 @@
|
|||||||
import React, { useState } from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import classnames from 'classnames';
|
|
||||||
|
|
||||||
import { useSnackbarContext, TabFooter, HotkeyField } from '@ohif/ui';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
|
|
||||||
import { hotkeysValidators } from './hotkeysValidators';
|
|
||||||
import { MODIFIER_KEYS } from './hotkeysConfig';
|
|
||||||
|
|
||||||
import { hotkeysManager } from '../../App';
|
|
||||||
|
|
||||||
import './HotkeysPreferences.styl';
|
|
||||||
/**
|
|
||||||
* Take hotkeyDefenintions and build an initialState to be used into the component state
|
|
||||||
*
|
|
||||||
* @param {Object} hotkeyDefinitions
|
|
||||||
* @returns {Object} initialState
|
|
||||||
*/
|
|
||||||
const initialState = hotkeyDefinitions => ({
|
|
||||||
hotkeys: { ...hotkeyDefinitions },
|
|
||||||
errors: {},
|
|
||||||
});
|
|
||||||
/**
|
|
||||||
* Take the updated command and keys and validate the changes with all validators
|
|
||||||
*
|
|
||||||
* @param {Object} arguments
|
|
||||||
* @param {string} arguments.commandName command name string to be updated
|
|
||||||
* @param {array} arguments.pressedKeys new array of keys to be added for the commandName
|
|
||||||
* @param {array} arguments.hotkeys all hotkeys currently into the app
|
|
||||||
* @returns {Object} {errorMessage} errorMessage coming from any of the validator or undefined if none
|
|
||||||
*/
|
|
||||||
const validateCommandKey = ({ commandName, pressedKeys, hotkeys }) => {
|
|
||||||
for (const validator of hotkeysValidators) {
|
|
||||||
const validation = validator({
|
|
||||||
commandName,
|
|
||||||
pressedKeys,
|
|
||||||
hotkeys,
|
|
||||||
});
|
|
||||||
if (validation && validation.hasError) {
|
|
||||||
return validation;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
errorMessage: undefined,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Take all hotkeys and split the list into two lists
|
|
||||||
*
|
|
||||||
* @param {array} hotkeys list of all hotkeys
|
|
||||||
* @returns {array} array containing two arrays of keys
|
|
||||||
*/
|
|
||||||
const splitHotkeys = hotkeys => {
|
|
||||||
const splitedHotkeys = [];
|
|
||||||
const arrayHotkeys = Object.entries(hotkeys);
|
|
||||||
|
|
||||||
if (arrayHotkeys.length) {
|
|
||||||
const halfwayThrough = Math.ceil(arrayHotkeys.length / 2);
|
|
||||||
splitedHotkeys.push(arrayHotkeys.slice(0, halfwayThrough));
|
|
||||||
splitedHotkeys.push(
|
|
||||||
arrayHotkeys.slice(halfwayThrough, arrayHotkeys.length)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return splitedHotkeys;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* HotkeysPreferences tab
|
|
||||||
* It renders all hotkeys displayed into columns/rows
|
|
||||||
*
|
|
||||||
* It stores current state and whenever it changes, component messages parent of new value (through function callback)
|
|
||||||
* @param {object} props component props
|
|
||||||
* @param {string} props.onClose
|
|
||||||
*/
|
|
||||||
function HotkeysPreferences({ onClose }) {
|
|
||||||
const { t } = useTranslation('UserPreferencesModal');
|
|
||||||
const { hotkeyDefaults, hotkeyDefinitions } = hotkeysManager;
|
|
||||||
|
|
||||||
const [state, setState] = useState(initialState(hotkeyDefinitions));
|
|
||||||
|
|
||||||
const snackbar = useSnackbarContext();
|
|
||||||
|
|
||||||
const onResetPreferences = () => {
|
|
||||||
const defaultHotKeyDefinitions = {};
|
|
||||||
|
|
||||||
hotkeyDefaults.map(item => {
|
|
||||||
const { commandName, ...values } = item;
|
|
||||||
defaultHotKeyDefinitions[commandName] = { ...values };
|
|
||||||
});
|
|
||||||
|
|
||||||
setState(initialState(defaultHotKeyDefinitions));
|
|
||||||
};
|
|
||||||
|
|
||||||
const onSave = () => {
|
|
||||||
const { hotkeys } = state;
|
|
||||||
|
|
||||||
hotkeysManager.setHotkeys(hotkeys);
|
|
||||||
|
|
||||||
localStorage.setItem('hotkey-definitions', JSON.stringify(hotkeys));
|
|
||||||
|
|
||||||
onClose();
|
|
||||||
|
|
||||||
snackbar.show({
|
|
||||||
message: t('SaveMessage'),
|
|
||||||
type: 'success',
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const onHotkeyChanged = (commandName, hotkeyDefinition, keys) => {
|
|
||||||
const { errorMessage } = validateCommandKey({
|
|
||||||
commandName,
|
|
||||||
pressedKeys: keys,
|
|
||||||
hotkeys: state.hotkeys,
|
|
||||||
});
|
|
||||||
|
|
||||||
setState(prevState => ({
|
|
||||||
hotkeys: {
|
|
||||||
...prevState.hotkeys,
|
|
||||||
[commandName]: { ...hotkeyDefinition, keys },
|
|
||||||
},
|
|
||||||
errors: {
|
|
||||||
...prevState.errors,
|
|
||||||
[commandName]: errorMessage,
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
const hasErrors = Object.keys(state.errors).some(key => !!state.errors[key]);
|
|
||||||
const hasHotkeys = Object.keys(state.hotkeys).length;
|
|
||||||
const splitedHotkeys = splitHotkeys(state.hotkeys);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<React.Fragment>
|
|
||||||
<div className="HotkeysPreferences">
|
|
||||||
{hasHotkeys ? (
|
|
||||||
<div className="hotkeyTable">
|
|
||||||
{splitedHotkeys.map((hotkeys, index) => {
|
|
||||||
return (
|
|
||||||
<div className="hotkeyColumn" key={index}>
|
|
||||||
<div className="hotkeyHeader">
|
|
||||||
<div className="headerItemText text-right">Function</div>
|
|
||||||
<div className="headerItemText text-center">Shortcut</div>
|
|
||||||
</div>
|
|
||||||
{hotkeys.map(hotkey => {
|
|
||||||
const commandName = hotkey[0];
|
|
||||||
const hotkeyDefinition = hotkey[1];
|
|
||||||
const { keys, label } = hotkeyDefinition;
|
|
||||||
const errorMessage = state.errors[hotkey[0]];
|
|
||||||
const handleChange = keys => {
|
|
||||||
onHotkeyChanged(commandName, hotkeyDefinition, keys);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div key={commandName} className="hotkeyRow">
|
|
||||||
<div className="hotkeyLabel">{label}</div>
|
|
||||||
<div
|
|
||||||
data-key="defaultTool"
|
|
||||||
className={classnames(
|
|
||||||
'wrapperHotkeyInput',
|
|
||||||
errorMessage ? 'stateError' : ''
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<HotkeyField
|
|
||||||
keys={keys}
|
|
||||||
modifier_keys={MODIFIER_KEYS}
|
|
||||||
handleChange={handleChange}
|
|
||||||
classNames={'preferencesInput'}
|
|
||||||
></HotkeyField>
|
|
||||||
<span className="preferencesInputErrorMessage">
|
|
||||||
{errorMessage}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
'Hotkeys definitions is empty'
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<TabFooter
|
|
||||||
onResetPreferences={onResetPreferences}
|
|
||||||
onSave={onSave}
|
|
||||||
onCancel={onClose}
|
|
||||||
hasErrors={hasErrors}
|
|
||||||
t={t}
|
|
||||||
/>
|
|
||||||
</React.Fragment>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
HotkeysPreferences.propTypes = {
|
|
||||||
onClose: PropTypes.func,
|
|
||||||
};
|
|
||||||
|
|
||||||
export { HotkeysPreferences };
|
|
||||||
@ -1,46 +0,0 @@
|
|||||||
.HotkeysPreferences
|
|
||||||
display: flex
|
|
||||||
padding: 20px
|
|
||||||
|
|
||||||
.hotkeyTable
|
|
||||||
display: flex
|
|
||||||
flex-direction: row
|
|
||||||
flex-basis: 0
|
|
||||||
flex-grow: 1.5
|
|
||||||
|
|
||||||
.hotkeyColumn
|
|
||||||
display: flex
|
|
||||||
flex-direction: column
|
|
||||||
flex-basis: 0
|
|
||||||
flex-grow: 1.5
|
|
||||||
|
|
||||||
.hotkeyHeader
|
|
||||||
display: flex
|
|
||||||
flex-direction: row
|
|
||||||
margin-bottom: 10px
|
|
||||||
|
|
||||||
.headerItemText
|
|
||||||
flex-basis: 0
|
|
||||||
flex-grow: 1.5
|
|
||||||
padding: 5px 15px 5px 0
|
|
||||||
|
|
||||||
.hotkeyRow
|
|
||||||
display: flex
|
|
||||||
|
|
||||||
.wrapperHotkeyInput
|
|
||||||
margin-bottom: 5px
|
|
||||||
cursor: pointer
|
|
||||||
display: flex
|
|
||||||
flex-direction: column
|
|
||||||
flex-basis: 0
|
|
||||||
flex-grow: 1.5
|
|
||||||
|
|
||||||
.hotkeyLabel
|
|
||||||
padding: 5px 15px 5px 0
|
|
||||||
text-align: right
|
|
||||||
flex-basis: 0
|
|
||||||
flex-grow: 1.5
|
|
||||||
|
|
||||||
.stateError
|
|
||||||
.preferencesInput
|
|
||||||
background-color: var(--state-error)
|
|
||||||
@ -1,42 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import { TabComponents } from '@ohif/ui';
|
|
||||||
|
|
||||||
// Tabs
|
|
||||||
import { HotkeysPreferences } from './HotkeysPreferences';
|
|
||||||
import { WindowLevelPreferences } from './WindowLevelPreferences';
|
|
||||||
import { GeneralPreferences } from './GeneralPreferences';
|
|
||||||
|
|
||||||
import './UserPreferences.styl';
|
|
||||||
|
|
||||||
const tabs = [
|
|
||||||
{
|
|
||||||
name: 'Hotkeys',
|
|
||||||
Component: HotkeysPreferences,
|
|
||||||
customProps: {},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'General',
|
|
||||||
Component: GeneralPreferences,
|
|
||||||
customProps: {},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Window Level',
|
|
||||||
Component: WindowLevelPreferences,
|
|
||||||
customProps: {},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
function UserPreferences({ hide }) {
|
|
||||||
const customProps = {
|
|
||||||
onClose: hide,
|
|
||||||
};
|
|
||||||
return <TabComponents tabs={tabs} customProps={customProps} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
UserPreferences.propTypes = {
|
|
||||||
hide: PropTypes.func,
|
|
||||||
};
|
|
||||||
|
|
||||||
export { UserPreferences };
|
|
||||||
@ -1,26 +0,0 @@
|
|||||||
.preferencesInputErrorMessage
|
|
||||||
color: var(--state-error-text)
|
|
||||||
font-size: 10px
|
|
||||||
text-transform: uppercase
|
|
||||||
|
|
||||||
.preferencesInput
|
|
||||||
font-weight: 400
|
|
||||||
cursor: pointer
|
|
||||||
transition: background-color .3s ease,border-color .3s ease
|
|
||||||
background-color: var(--ui-gray)
|
|
||||||
color: var(--text-primary-color)
|
|
||||||
border-color: var(--ui-border-coolor)
|
|
||||||
border: 0
|
|
||||||
border-radius: 2px
|
|
||||||
font-size: 14px
|
|
||||||
height: 30px
|
|
||||||
width: 100%
|
|
||||||
line-height: 16px
|
|
||||||
padding: 8px 9px 6px
|
|
||||||
text-align: center
|
|
||||||
|
|
||||||
.preferencesInput:focus
|
|
||||||
border-color: var(--active-color)
|
|
||||||
background-color: var(--ui-gray-dark)
|
|
||||||
box-shadow: 0 0 0 2px var(--active-color) !important
|
|
||||||
outline: 0
|
|
||||||
@ -1,128 +0,0 @@
|
|||||||
import React, { useState } from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { useSelector, useDispatch } from 'react-redux';
|
|
||||||
import { redux } from '@ohif/core';
|
|
||||||
|
|
||||||
import { TabFooter, useSnackbarContext } from '@ohif/ui';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
|
|
||||||
const { actions } = redux;
|
|
||||||
|
|
||||||
import './WindowLevelPreferences.styl';
|
|
||||||
|
|
||||||
function WindowLevelPreferences({ onClose }) {
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
|
|
||||||
const windowLevelData = useSelector(state => {
|
|
||||||
const { preferences = {} } = state;
|
|
||||||
const { windowLevelData } = preferences;
|
|
||||||
|
|
||||||
return windowLevelData;
|
|
||||||
});
|
|
||||||
|
|
||||||
const [state, setState] = useState({
|
|
||||||
values: { ...windowLevelData },
|
|
||||||
});
|
|
||||||
|
|
||||||
const { t } = useTranslation('UserPreferencesModal');
|
|
||||||
const onResetPreferences = () => {};
|
|
||||||
const hasErrors = false;
|
|
||||||
const onSave = () => {
|
|
||||||
dispatch(actions.setUserPreferences({ windowLevelData: state.values }));
|
|
||||||
|
|
||||||
onClose();
|
|
||||||
|
|
||||||
snackbar.show({
|
|
||||||
message: t('SaveMessage'),
|
|
||||||
type: 'success',
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const snackbar = useSnackbarContext();
|
|
||||||
|
|
||||||
const handleInputChange = event => {
|
|
||||||
const $target = event.target;
|
|
||||||
const { key, inputname } = $target.dataset;
|
|
||||||
const inputValue = $target.value;
|
|
||||||
|
|
||||||
if (!state.values[key] || !state.values[key][inputname]) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setState(prevState => ({
|
|
||||||
...prevState,
|
|
||||||
values: {
|
|
||||||
...prevState.values,
|
|
||||||
[key]: {
|
|
||||||
...prevState.values[key],
|
|
||||||
[inputname]: inputValue,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<React.Fragment>
|
|
||||||
<div className="WindowLevelPreferences">
|
|
||||||
<div className="wlColumn">
|
|
||||||
<div className="wlRow header">
|
|
||||||
<div className="wlColumn preset">Preset</div>
|
|
||||||
<div className="wlColumn description">Description</div>
|
|
||||||
<div className="wlColumn window">Window</div>
|
|
||||||
<div className="wlColumn level">Level</div>
|
|
||||||
</div>
|
|
||||||
{Object.keys(state.values).map((key, index) => {
|
|
||||||
return (
|
|
||||||
<div className="wlRow" key={key}>
|
|
||||||
<div className="wlColumn preset">{key}</div>
|
|
||||||
<div className="wlColumn description">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
className="preferencesInput"
|
|
||||||
value={state.values[key].description}
|
|
||||||
data-key={key}
|
|
||||||
data-inputname="description"
|
|
||||||
onChange={handleInputChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="wlColumn window">
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
className="preferencesInput"
|
|
||||||
value={state.values[key].window}
|
|
||||||
data-key={key}
|
|
||||||
data-inputname="window"
|
|
||||||
onChange={handleInputChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="wlColumn level">
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
className="preferencesInput"
|
|
||||||
value={state.values[key].level}
|
|
||||||
data-key={key}
|
|
||||||
data-inputname="level"
|
|
||||||
onChange={handleInputChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<TabFooter
|
|
||||||
onResetPreferences={onResetPreferences}
|
|
||||||
onSave={onSave}
|
|
||||||
onCancel={onClose}
|
|
||||||
hasErrors={hasErrors}
|
|
||||||
t={t}
|
|
||||||
/>
|
|
||||||
</React.Fragment>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
WindowLevelPreferences.propTypes = {
|
|
||||||
onClose: PropTypes.func,
|
|
||||||
};
|
|
||||||
|
|
||||||
export { WindowLevelPreferences };
|
|
||||||
@ -1,28 +0,0 @@
|
|||||||
.WindowLevelPreferences
|
|
||||||
display: flex
|
|
||||||
padding: 20px 0
|
|
||||||
text-align: center
|
|
||||||
|
|
||||||
.wlColumn
|
|
||||||
display: flex
|
|
||||||
flex-direction: column
|
|
||||||
flex-basis: 0
|
|
||||||
flex-grow: 1.5
|
|
||||||
margin: 0 5px
|
|
||||||
|
|
||||||
.wlRow
|
|
||||||
display: flex
|
|
||||||
flex-direction: row
|
|
||||||
margin: 5px 0
|
|
||||||
|
|
||||||
.header
|
|
||||||
|
|
||||||
.preset
|
|
||||||
flex-grow: 0.5
|
|
||||||
|
|
||||||
.window,
|
|
||||||
.level
|
|
||||||
flex-grow: 1
|
|
||||||
|
|
||||||
.description
|
|
||||||
flex-grow: 2
|
|
||||||
@ -1,91 +0,0 @@
|
|||||||
const range = (start, end) => {
|
|
||||||
return new Array(end - start).fill().map((d, i) => i + start);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const MODIFIER_KEYS = ['ctrl', 'alt', 'shift'];
|
|
||||||
|
|
||||||
export const DISALLOWED_COMBINATIONS = {
|
|
||||||
'': [],
|
|
||||||
alt: ['space'],
|
|
||||||
shift: [],
|
|
||||||
ctrl: [
|
|
||||||
'f4',
|
|
||||||
'f5',
|
|
||||||
'f11',
|
|
||||||
'w',
|
|
||||||
'r',
|
|
||||||
't',
|
|
||||||
'o',
|
|
||||||
'p',
|
|
||||||
'a',
|
|
||||||
'd',
|
|
||||||
'f',
|
|
||||||
'g',
|
|
||||||
'h',
|
|
||||||
'j',
|
|
||||||
'l',
|
|
||||||
'z',
|
|
||||||
'x',
|
|
||||||
'c',
|
|
||||||
'v',
|
|
||||||
'b',
|
|
||||||
'n',
|
|
||||||
'pagedown',
|
|
||||||
'pageup',
|
|
||||||
],
|
|
||||||
'ctrl+shift': ['q', 'w', 'r', 't', 'p', 'a', 'h', 'v', 'b', 'n'],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const SPECIAL_KEYS = {
|
|
||||||
8: 'backspace',
|
|
||||||
9: 'tab',
|
|
||||||
13: 'return',
|
|
||||||
16: 'shift',
|
|
||||||
17: 'ctrl',
|
|
||||||
18: 'alt',
|
|
||||||
19: 'pause',
|
|
||||||
20: 'capslock',
|
|
||||||
27: 'esc',
|
|
||||||
32: 'space',
|
|
||||||
33: 'pageup',
|
|
||||||
34: 'pagedown',
|
|
||||||
35: 'end',
|
|
||||||
36: 'home',
|
|
||||||
37: 'left',
|
|
||||||
38: 'up',
|
|
||||||
39: 'right',
|
|
||||||
40: 'down',
|
|
||||||
45: 'insert',
|
|
||||||
46: 'del',
|
|
||||||
96: '0',
|
|
||||||
97: '1',
|
|
||||||
98: '2',
|
|
||||||
99: '3',
|
|
||||||
100: '4',
|
|
||||||
101: '5',
|
|
||||||
102: '6',
|
|
||||||
103: '7',
|
|
||||||
104: '8',
|
|
||||||
105: '9',
|
|
||||||
106: '*',
|
|
||||||
107: '+',
|
|
||||||
109: '-',
|
|
||||||
110: '.',
|
|
||||||
111: '/',
|
|
||||||
112: 'f1',
|
|
||||||
113: 'f2',
|
|
||||||
114: 'f3',
|
|
||||||
115: 'f4',
|
|
||||||
116: 'f5',
|
|
||||||
117: 'f6',
|
|
||||||
118: 'f7',
|
|
||||||
119: 'f8',
|
|
||||||
120: 'f9',
|
|
||||||
121: 'f10',
|
|
||||||
122: 'f11',
|
|
||||||
123: 'f12',
|
|
||||||
144: 'numlock',
|
|
||||||
145: 'scroll',
|
|
||||||
191: '/',
|
|
||||||
224: 'meta',
|
|
||||||
};
|
|
||||||
@ -1,97 +0,0 @@
|
|||||||
import { MODIFIER_KEYS, DISALLOWED_COMBINATIONS } from './hotkeysConfig';
|
|
||||||
|
|
||||||
const formatPressedKeys = pressedKeysArray => pressedKeysArray.join('+');
|
|
||||||
|
|
||||||
const findConflictingCommand = (hotkeys, currentCommandName, pressedKeys) => {
|
|
||||||
let firstConflictingCommand = undefined;
|
|
||||||
const formatedPressedHotkeys = formatPressedKeys(pressedKeys);
|
|
||||||
|
|
||||||
for (const commandName in hotkeys) {
|
|
||||||
const toolHotkeys = hotkeys[commandName].keys;
|
|
||||||
const formatedToolHotkeys = formatPressedKeys(toolHotkeys);
|
|
||||||
|
|
||||||
if (
|
|
||||||
formatedPressedHotkeys === formatedToolHotkeys &&
|
|
||||||
commandName !== currentCommandName
|
|
||||||
) {
|
|
||||||
firstConflictingCommand = hotkeys[commandName];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return firstConflictingCommand;
|
|
||||||
};
|
|
||||||
|
|
||||||
const ERROR_MESSAGES = {
|
|
||||||
MODIFIER:
|
|
||||||
"It's not possible to define only modifier keys (ctrl, alt and shift) as a shortcut",
|
|
||||||
EMPTY: "Field can't be empty.",
|
|
||||||
};
|
|
||||||
|
|
||||||
// VALIDATORS
|
|
||||||
|
|
||||||
const modifierValidator = ({ pressedKeys }) => {
|
|
||||||
const lastPressedKey = pressedKeys[pressedKeys.length - 1];
|
|
||||||
// Check if it has a valid modifier
|
|
||||||
const isModifier = MODIFIER_KEYS.includes(lastPressedKey);
|
|
||||||
if (isModifier) {
|
|
||||||
return {
|
|
||||||
hasError: true,
|
|
||||||
errorMessage: ERROR_MESSAGES.MODIFIER,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const emptyValidator = ({ pressedKeys = [] }) => {
|
|
||||||
if (!pressedKeys.length) {
|
|
||||||
return {
|
|
||||||
hasError: true,
|
|
||||||
errorMessage: ERROR_MESSAGES.EMPTY,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const conflictingValidator = ({ commandName, pressedKeys, hotkeys }) => {
|
|
||||||
const conflictingCommand = findConflictingCommand(
|
|
||||||
hotkeys,
|
|
||||||
commandName,
|
|
||||||
pressedKeys
|
|
||||||
);
|
|
||||||
|
|
||||||
if (conflictingCommand) {
|
|
||||||
return {
|
|
||||||
hasError: true,
|
|
||||||
errorMessage: `"${conflictingCommand.label}" is already using the "${pressedKeys}" shortcut.`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const disallowedValidator = ({ pressedKeys = [] }) => {
|
|
||||||
const lastPressedKey = pressedKeys[pressedKeys.length - 1];
|
|
||||||
const modifierCommand = formatPressedKeys(
|
|
||||||
pressedKeys.slice(0, pressedKeys.length - 1)
|
|
||||||
);
|
|
||||||
|
|
||||||
const disallowedCombination = DISALLOWED_COMBINATIONS[modifierCommand];
|
|
||||||
const hasDisallowedCombinations = disallowedCombination
|
|
||||||
? disallowedCombination.includes(lastPressedKey)
|
|
||||||
: false;
|
|
||||||
|
|
||||||
if (hasDisallowedCombinations) {
|
|
||||||
return {
|
|
||||||
hasError: true,
|
|
||||||
errorMessage: `"${formatPressedKeys(
|
|
||||||
pressedKeys
|
|
||||||
)}" shortcut combination is not allowed`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const hotkeysValidators = [
|
|
||||||
emptyValidator,
|
|
||||||
modifierValidator,
|
|
||||||
conflictingValidator,
|
|
||||||
disallowedValidator,
|
|
||||||
];
|
|
||||||
|
|
||||||
export { hotkeysValidators };
|
|
||||||
@ -1 +0,0 @@
|
|||||||
export { UserPreferences } from './UserPreferences';
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
/**
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* @export
|
|
||||||
* @param {*} props
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export default function DefaultViewport(props) {
|
|
||||||
return <div>{JSON.stringify(props)}</div>;
|
|
||||||
}
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
.empty-viewport {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
height: 100%;
|
|
||||||
color: var(--text-secondary-color);
|
|
||||||
}
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
/**
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
function EmptyViewport() {
|
|
||||||
return (
|
|
||||||
<div className="empty-viewport">
|
|
||||||
<p>Please drag a stack here to view images.</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
.viewport-container {
|
|
||||||
float: left;
|
|
||||||
position: relative;
|
|
||||||
border: var(--viewport-border-thickness) solid var(--ui-border-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.viewport-container.active {
|
|
||||||
border: var(--viewport-border-thickness) solid var(--active-color);
|
|
||||||
}
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
.viewport-drop-target {
|
|
||||||
opacity: 1;
|
|
||||||
position: relative; /* Locks in Scrollbar */
|
|
||||||
transition: 0.3s all ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.viewport-drop-target.hovered {
|
|
||||||
opacity: 0.5;
|
|
||||||
cursor: copy;
|
|
||||||
}
|
|
||||||
@ -1,5 +0,0 @@
|
|||||||
import ConnectedViewportGrid from './ConnectedViewportGrid.js';
|
|
||||||
import ViewportGrid from './ViewportGrid.js';
|
|
||||||
|
|
||||||
export default ViewportGrid;
|
|
||||||
export { ConnectedViewportGrid, ViewportGrid };
|
|
||||||
@ -1,104 +0,0 @@
|
|||||||
import { connect } from 'react-redux';
|
|
||||||
import { CineDialog } from '@ohif/ui';
|
|
||||||
import OHIF from '@ohif/core';
|
|
||||||
import csTools from 'cornerstone-tools';
|
|
||||||
import { commandsManager } from './../App.js';
|
|
||||||
// Our target output kills the `as` and "import" throws a keyword error
|
|
||||||
// import { import as toolImport, getToolState } from 'cornerstone-tools';
|
|
||||||
import cloneDeep from 'lodash.clonedeep';
|
|
||||||
|
|
||||||
const toolImport = csTools.import;
|
|
||||||
const scrollToIndex = toolImport('util/scrollToIndex');
|
|
||||||
const { setViewportSpecificData } = OHIF.redux.actions;
|
|
||||||
|
|
||||||
// Why do I need or care about any of this info?
|
|
||||||
// A dispatch action should be able to pull this at the time of an event?
|
|
||||||
// `isPlaying` and `cineFrameRate` might matter, but I think we can prop pass for those.
|
|
||||||
const mapStateToProps = state => {
|
|
||||||
// Get activeViewport's `cine` and `stack`
|
|
||||||
const { viewportSpecificData, activeViewportIndex } = state.viewports;
|
|
||||||
const { cine } = viewportSpecificData[activeViewportIndex] || {};
|
|
||||||
const dom = commandsManager.runCommand('getActiveViewportEnabledElement');
|
|
||||||
|
|
||||||
const cineData = cine || {
|
|
||||||
isPlaying: false,
|
|
||||||
cineFrameRate: 24,
|
|
||||||
};
|
|
||||||
|
|
||||||
// New props we're creating?
|
|
||||||
return {
|
|
||||||
activeEnabledElement: dom,
|
|
||||||
activeViewportCineData: cineData,
|
|
||||||
activeViewportIndex: state.viewports.activeViewportIndex,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const mapDispatchToProps = dispatch => {
|
|
||||||
return {
|
|
||||||
dispatchSetViewportSpecificData: (viewportIndex, data) => {
|
|
||||||
dispatch(setViewportSpecificData(viewportIndex, data));
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const mergeProps = (propsFromState, propsFromDispatch, ownProps) => {
|
|
||||||
const {
|
|
||||||
activeEnabledElement,
|
|
||||||
activeViewportCineData,
|
|
||||||
activeViewportIndex,
|
|
||||||
} = propsFromState;
|
|
||||||
|
|
||||||
return {
|
|
||||||
cineFrameRate: activeViewportCineData.cineFrameRate,
|
|
||||||
isPlaying: activeViewportCineData.isPlaying,
|
|
||||||
onPlayPauseChanged: isPlaying => {
|
|
||||||
const cine = cloneDeep(activeViewportCineData);
|
|
||||||
cine.isPlaying = !cine.isPlaying;
|
|
||||||
|
|
||||||
propsFromDispatch.dispatchSetViewportSpecificData(activeViewportIndex, {
|
|
||||||
cine,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onFrameRateChanged: frameRate => {
|
|
||||||
const cine = cloneDeep(activeViewportCineData);
|
|
||||||
cine.cineFrameRate = frameRate;
|
|
||||||
|
|
||||||
propsFromDispatch.dispatchSetViewportSpecificData(activeViewportIndex, {
|
|
||||||
cine,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onClickNextButton: () => {
|
|
||||||
const stackData = csTools.getToolState(activeEnabledElement, 'stack');
|
|
||||||
if (!stackData || !stackData.data || !stackData.data.length) return;
|
|
||||||
const { currentImageIdIndex, imageIds } = stackData.data[0];
|
|
||||||
if (currentImageIdIndex >= imageIds.length - 1) return;
|
|
||||||
scrollToIndex(activeEnabledElement, currentImageIdIndex + 1);
|
|
||||||
},
|
|
||||||
onClickBackButton: () => {
|
|
||||||
const stackData = csTools.getToolState(activeEnabledElement, 'stack');
|
|
||||||
if (!stackData || !stackData.data || !stackData.data.length) return;
|
|
||||||
const { currentImageIdIndex } = stackData.data[0];
|
|
||||||
if (currentImageIdIndex === 0) return;
|
|
||||||
scrollToIndex(activeEnabledElement, currentImageIdIndex - 1);
|
|
||||||
},
|
|
||||||
onClickSkipToStart: () => {
|
|
||||||
const stackData = csTools.getToolState(activeEnabledElement, 'stack');
|
|
||||||
if (!stackData || !stackData.data || !stackData.data.length) return;
|
|
||||||
scrollToIndex(activeEnabledElement, 0);
|
|
||||||
},
|
|
||||||
onClickSkipToEnd: () => {
|
|
||||||
const stackData = csTools.getToolState(activeEnabledElement, 'stack');
|
|
||||||
if (!stackData || !stackData.data || !stackData.data.length) return;
|
|
||||||
const lastIndex = stackData.data[0].imageIds.length - 1;
|
|
||||||
scrollToIndex(activeEnabledElement, lastIndex);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const ConnectedCineDialog = connect(
|
|
||||||
mapStateToProps,
|
|
||||||
mapDispatchToProps,
|
|
||||||
mergeProps
|
|
||||||
)(CineDialog);
|
|
||||||
|
|
||||||
export default ConnectedCineDialog;
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
import Header from '../components/Header/Header.js';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
|
|
||||||
const mapStateToProps = state => {
|
|
||||||
return {
|
|
||||||
user: state.oidc && state.oidc.user,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const ConnectedHeader = connect(mapStateToProps)(Header);
|
|
||||||
|
|
||||||
export default ConnectedHeader;
|
|
||||||
@ -1,67 +0,0 @@
|
|||||||
import { LayoutButton } from '@ohif/ui';
|
|
||||||
import OHIF from '@ohif/core';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
|
|
||||||
const { setLayout, setViewportActive } = OHIF.redux.actions;
|
|
||||||
|
|
||||||
const mapStateToProps = state => {
|
|
||||||
return {
|
|
||||||
currentLayout: state.viewports.layout,
|
|
||||||
activeViewportIndex: state.viewports.activeViewportIndex,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const mapDispatchToProps = dispatch => {
|
|
||||||
return {
|
|
||||||
// TODO: Change if layout switched becomes more complex
|
|
||||||
onChange: (selectedCell, currentLayout, activeViewportIndex) => {
|
|
||||||
const viewports = [];
|
|
||||||
const numRows = selectedCell.row + 1;
|
|
||||||
const numColumns = selectedCell.col + 1;
|
|
||||||
const numViewports = numRows * numColumns;
|
|
||||||
|
|
||||||
for (let i = 0; i < numViewports; i++) {
|
|
||||||
// Hacky way to allow users to exit MPR "mode"
|
|
||||||
const viewport = currentLayout.viewports[i];
|
|
||||||
let plugin = viewport && viewport.plugin;
|
|
||||||
if (viewport && viewport.vtk) {
|
|
||||||
plugin = 'cornerstone';
|
|
||||||
}
|
|
||||||
|
|
||||||
viewports.push({
|
|
||||||
plugin,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const layout = {
|
|
||||||
numRows,
|
|
||||||
numColumns,
|
|
||||||
viewports,
|
|
||||||
};
|
|
||||||
|
|
||||||
const maxActiveIndex = numViewports - 1;
|
|
||||||
if (activeViewportIndex > maxActiveIndex) {
|
|
||||||
dispatch(setViewportActive(0));
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatch(setLayout(layout));
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const mergeProps = (propsFromState, propsFromDispatch) => {
|
|
||||||
const onChangeFromDispatch = propsFromDispatch.onChange;
|
|
||||||
const { currentLayout, activeViewportIndex } = propsFromState;
|
|
||||||
|
|
||||||
return {
|
|
||||||
onChange: selectedCell =>
|
|
||||||
onChangeFromDispatch(selectedCell, currentLayout, activeViewportIndex),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const ConnectedLayoutButton = connect(
|
|
||||||
mapStateToProps,
|
|
||||||
mapDispatchToProps,
|
|
||||||
mergeProps
|
|
||||||
)(LayoutButton);
|
|
||||||
|
|
||||||
export default ConnectedLayoutButton;
|
|
||||||
@ -1,21 +0,0 @@
|
|||||||
import { connect } from 'react-redux';
|
|
||||||
import StandaloneRouting from '../routes/StandaloneRouting';
|
|
||||||
|
|
||||||
const mapDispatchToProps = dispatch => {
|
|
||||||
return {
|
|
||||||
activateServer: server => {
|
|
||||||
const action = {
|
|
||||||
type: 'ACTIVATE_SERVER',
|
|
||||||
server,
|
|
||||||
};
|
|
||||||
dispatch(action);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const ConnectedStandaloneRouting = connect(
|
|
||||||
null,
|
|
||||||
mapDispatchToProps
|
|
||||||
)(StandaloneRouting);
|
|
||||||
|
|
||||||
export default ConnectedStandaloneRouting;
|
|
||||||
@ -1,14 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { StudyList } from '@ohif/ui';
|
|
||||||
|
|
||||||
const ConnectedStudyList = () => {
|
|
||||||
return (
|
|
||||||
/** Temporary using StudyList -- We should mount it by using the created
|
|
||||||
* components such as NavBar, InputGroup, StudyListTable, etc...
|
|
||||||
* Check "Views > StudyList" in @ohif/ui for reference
|
|
||||||
*/
|
|
||||||
<StudyList />
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ConnectedStudyList;
|
|
||||||
@ -1,26 +0,0 @@
|
|||||||
import { connect } from "react-redux";
|
|
||||||
import StudyLoadingMonitor from "../components/StudyLoadingMonitor.js";
|
|
||||||
import OHIF from "@ohif/core";
|
|
||||||
|
|
||||||
const {
|
|
||||||
setStudyLoadingProgress,
|
|
||||||
clearStudyLoadingProgress
|
|
||||||
} = OHIF.redux.actions;
|
|
||||||
|
|
||||||
const mapDispatchToProps = dispatch => {
|
|
||||||
return {
|
|
||||||
setStudyLoadingProgress: (progressId, progressData) => {
|
|
||||||
dispatch(setStudyLoadingProgress(progressId, progressData));
|
|
||||||
},
|
|
||||||
clearStudyLoadingProgress: progressId => {
|
|
||||||
dispatch(clearStudyLoadingProgress(progressId));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const ConnectedStudyLoadingMonitor = connect(
|
|
||||||
null,
|
|
||||||
mapDispatchToProps
|
|
||||||
)(StudyLoadingMonitor);
|
|
||||||
|
|
||||||
export default ConnectedStudyLoadingMonitor;
|
|
||||||
@ -1,37 +0,0 @@
|
|||||||
import { connect } from 'react-redux';
|
|
||||||
import Viewer from './Viewer.js';
|
|
||||||
import OHIF from '@ohif/core';
|
|
||||||
|
|
||||||
const { setTimepoints, setMeasurements } = OHIF.redux.actions;
|
|
||||||
|
|
||||||
const getActiveServer = servers => {
|
|
||||||
const isActive = a => a.active === true;
|
|
||||||
return servers.servers.find(isActive);
|
|
||||||
};
|
|
||||||
|
|
||||||
const mapStateToProps = state => {
|
|
||||||
const { viewports, servers } = state;
|
|
||||||
return {
|
|
||||||
viewports: viewports.viewportSpecificData,
|
|
||||||
activeViewportIndex: viewports.activeViewportIndex,
|
|
||||||
activeServer: getActiveServer(servers),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const mapDispatchToProps = dispatch => {
|
|
||||||
return {
|
|
||||||
onTimepointsUpdated: timepoints => {
|
|
||||||
dispatch(setTimepoints(timepoints));
|
|
||||||
},
|
|
||||||
onMeasurementsUpdated: measurements => {
|
|
||||||
dispatch(setMeasurements(measurements));
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const ConnectedViewer = connect(
|
|
||||||
mapStateToProps,
|
|
||||||
mapDispatchToProps
|
|
||||||
)(Viewer);
|
|
||||||
|
|
||||||
export default ConnectedViewer;
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
.ToolbarRow {
|
|
||||||
border-bottom: var(--ui-border-thickness) solid var(--ui-border-color);
|
|
||||||
flex: 0 0 auto;
|
|
||||||
height: var(--toolbar-height);
|
|
||||||
padding-left: 1rem;
|
|
||||||
padding-right: 1rem;
|
|
||||||
position: relative;
|
|
||||||
transition: height 300ms ease;
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
ToolbarRow.expanded {
|
|
||||||
height: calc(var(--toolbar-height) + var(--toolbar-drawer-height));
|
|
||||||
}
|
|
||||||
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
#viewer {
|
|
||||||
background-color: black;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loadingTextDiv {
|
|
||||||
color: var(--text-secondary-color);
|
|
||||||
font-size: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
.drag-drop-instructions {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.drag-drop-instructions h3 {
|
|
||||||
color: var(--active-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.drag-drop-instructions h4 {
|
|
||||||
color: var(--text-secondary-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.link-dialog {
|
|
||||||
color: var(--active-color);
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
@ -1,21 +0,0 @@
|
|||||||
#viewer {
|
|
||||||
height: calc(100% - var(--top-bar-height));
|
|
||||||
}
|
|
||||||
|
|
||||||
#viewer > .loadingTextDiv {
|
|
||||||
color: var(--text-secondary-color);
|
|
||||||
font-size: 30px;
|
|
||||||
height: 100%;
|
|
||||||
line-height: calc(100% - var(--top-bar-height));
|
|
||||||
}
|
|
||||||
|
|
||||||
.ViewerMain {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ViewerMain > div {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
@ -1,5 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
const UserManagerContext = React.createContext();
|
|
||||||
|
|
||||||
export default UserManagerContext;
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
import React, { useEffect, useRef } from 'react';
|
|
||||||
export default function usePrevious(value) {
|
|
||||||
const ref = useRef();
|
|
||||||
useEffect(() => {
|
|
||||||
ref.current = value;
|
|
||||||
}, [value]);
|
|
||||||
|
|
||||||
return ref.current;
|
|
||||||
}
|
|
||||||
@ -1,135 +0,0 @@
|
|||||||
import React, { useContext } from 'react';
|
|
||||||
import GoogleCloudApi from '../googleCloud/api/GoogleCloudApi';
|
|
||||||
import usePrevious from './usePrevious';
|
|
||||||
|
|
||||||
import * as GoogleCloudUtilServers from '../googleCloud/utils/getServers';
|
|
||||||
import { useSelector, useDispatch } from 'react-redux';
|
|
||||||
import isEqual from 'lodash.isequal';
|
|
||||||
|
|
||||||
// Contexts
|
|
||||||
import AppContext from '../context/AppContext';
|
|
||||||
|
|
||||||
const getActiveServer = servers => {
|
|
||||||
const isActive = a => a.active === true;
|
|
||||||
|
|
||||||
return servers && servers.servers && servers.servers.find(isActive);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getServers = (appConfig, project, location, dataset, dicomStore) => {
|
|
||||||
let servers = [];
|
|
||||||
if (appConfig.enableGoogleCloudAdapter) {
|
|
||||||
GoogleCloudApi.urlBase = appConfig.healthcareApiEndpoint;
|
|
||||||
const pathUrl = GoogleCloudApi.getUrlBaseDicomWeb(
|
|
||||||
project,
|
|
||||||
location,
|
|
||||||
dataset,
|
|
||||||
dicomStore
|
|
||||||
);
|
|
||||||
const data = {
|
|
||||||
project,
|
|
||||||
location,
|
|
||||||
dataset,
|
|
||||||
dicomStore,
|
|
||||||
wadoUriRoot: pathUrl,
|
|
||||||
qidoRoot: pathUrl,
|
|
||||||
wadoRoot: pathUrl,
|
|
||||||
};
|
|
||||||
servers = GoogleCloudUtilServers.getServers(data, dicomStore);
|
|
||||||
if (!isValidServer(servers[0], appConfig)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return servers;
|
|
||||||
};
|
|
||||||
|
|
||||||
const isValidServer = (server, appConfig) => {
|
|
||||||
if (appConfig.enableGoogleCloudAdapter) {
|
|
||||||
return GoogleCloudUtilServers.isValidServer(server);
|
|
||||||
}
|
|
||||||
|
|
||||||
return !!server;
|
|
||||||
};
|
|
||||||
|
|
||||||
const setServers = (dispatch, servers) => {
|
|
||||||
const action = {
|
|
||||||
type: 'SET_SERVERS',
|
|
||||||
servers,
|
|
||||||
};
|
|
||||||
dispatch(action);
|
|
||||||
};
|
|
||||||
|
|
||||||
const useServerFromUrl = (
|
|
||||||
servers = [],
|
|
||||||
previousServers,
|
|
||||||
activeServer,
|
|
||||||
urlBasedServers,
|
|
||||||
appConfig,
|
|
||||||
project,
|
|
||||||
location,
|
|
||||||
dataset,
|
|
||||||
dicomStore
|
|
||||||
) => {
|
|
||||||
// update state from url available only when gcloud on
|
|
||||||
if (!appConfig.enableGoogleCloudAdapter) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const serverHasChanged = previousServers !== servers && previousServers;
|
|
||||||
|
|
||||||
// do not update from url. use state instead.
|
|
||||||
if (serverHasChanged) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// if no valid urlbased servers
|
|
||||||
if (!urlBasedServers || !urlBasedServers.length) {
|
|
||||||
return false;
|
|
||||||
} else if (!servers.length || !activeServer) {
|
|
||||||
// no current valid server
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const newServer = urlBasedServers[0];
|
|
||||||
|
|
||||||
let exists = servers.some(
|
|
||||||
GoogleCloudUtilServers.isEqualServer.bind(undefined, newServer)
|
|
||||||
);
|
|
||||||
|
|
||||||
return !exists;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function useServer({
|
|
||||||
project,
|
|
||||||
location,
|
|
||||||
dataset,
|
|
||||||
dicomStore,
|
|
||||||
} = {}) {
|
|
||||||
// Hooks
|
|
||||||
const servers = useSelector(state => state && state.servers);
|
|
||||||
const previousServers = usePrevious(servers);
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
|
|
||||||
const { appConfig = {} } = useContext(AppContext);
|
|
||||||
|
|
||||||
const activeServer = getActiveServer(servers);
|
|
||||||
const urlBasedServers =
|
|
||||||
getServers(appConfig, project, location, dataset, dicomStore) || [];
|
|
||||||
const shouldUpdateServer = useServerFromUrl(
|
|
||||||
servers.servers,
|
|
||||||
previousServers,
|
|
||||||
activeServer,
|
|
||||||
urlBasedServers,
|
|
||||||
appConfig,
|
|
||||||
project,
|
|
||||||
location,
|
|
||||||
dataset,
|
|
||||||
dicomStore
|
|
||||||
);
|
|
||||||
|
|
||||||
if (shouldUpdateServer) {
|
|
||||||
setServers(dispatch, urlBasedServers);
|
|
||||||
} else if (isValidServer(activeServer, appConfig)) {
|
|
||||||
return activeServer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,26 +0,0 @@
|
|||||||
import { connect } from 'react-redux';
|
|
||||||
import DicomFileUploaderModal from './DicomFileUploaderModal.js';
|
|
||||||
import OHIF from '@ohif/core';
|
|
||||||
|
|
||||||
const isActive = a => a.active === true;
|
|
||||||
|
|
||||||
const mapStateToProps = state => {
|
|
||||||
const activeServer = state.servers.servers.find(isActive);
|
|
||||||
|
|
||||||
// TODO: Not sure I like this approach since it means we are recreating
|
|
||||||
// this function every time redux changes
|
|
||||||
const retrieveAuthHeaderFunction = () =>
|
|
||||||
OHIF.DICOMWeb.getAuthorizationHeader(activeServer);
|
|
||||||
|
|
||||||
return {
|
|
||||||
retrieveAuthHeaderFunction,
|
|
||||||
url: activeServer && activeServer.qidoRoot,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const ConnectedDicomFileUploader = connect(
|
|
||||||
mapStateToProps,
|
|
||||||
null
|
|
||||||
)(DicomFileUploaderModal);
|
|
||||||
|
|
||||||
export default ConnectedDicomFileUploader;
|
|
||||||
@ -1,32 +0,0 @@
|
|||||||
import { connect } from 'react-redux';
|
|
||||||
import DicomStorePickerModal from './DicomStorePickerModal.js';
|
|
||||||
|
|
||||||
const isActive = a => a.active === true;
|
|
||||||
|
|
||||||
const mapStateToProps = state => {
|
|
||||||
const activeServer = state.servers.servers.find(isActive);
|
|
||||||
|
|
||||||
return {
|
|
||||||
user: state.oidc && state.oidc.user,
|
|
||||||
url: activeServer && activeServer.qidoRoot,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const mapDispatchToProps = dispatch => {
|
|
||||||
return {
|
|
||||||
setServers: servers => {
|
|
||||||
const action = {
|
|
||||||
type: 'SET_SERVERS',
|
|
||||||
servers,
|
|
||||||
};
|
|
||||||
dispatch(action);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const ConnectedDicomStorePicker = connect(
|
|
||||||
mapStateToProps,
|
|
||||||
mapDispatchToProps
|
|
||||||
)(DicomStorePickerModal);
|
|
||||||
|
|
||||||
export default ConnectedDicomStorePicker;
|
|
||||||
@ -1,82 +0,0 @@
|
|||||||
import React, { Component } from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import './googleCloud.css';
|
|
||||||
import { withTranslation } from 'react-i18next';
|
|
||||||
import { Icon } from '@ohif/ui';
|
|
||||||
|
|
||||||
class DatasetsList extends Component {
|
|
||||||
state = {
|
|
||||||
search: '',
|
|
||||||
};
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
datasets: PropTypes.array,
|
|
||||||
loading: PropTypes.bool,
|
|
||||||
error: PropTypes.string,
|
|
||||||
onSelect: PropTypes.func,
|
|
||||||
};
|
|
||||||
|
|
||||||
static defaultProps = {
|
|
||||||
loading: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
renderTableRow = dataset => {
|
|
||||||
return (
|
|
||||||
<tr
|
|
||||||
key={dataset.name}
|
|
||||||
className={
|
|
||||||
this.state.highlightedItem === dataset.name
|
|
||||||
? 'noselect active'
|
|
||||||
: 'noselect'
|
|
||||||
}
|
|
||||||
onMouseEnter={() => {
|
|
||||||
this.onHighlightItem(dataset.name);
|
|
||||||
}}
|
|
||||||
onClick={() => {
|
|
||||||
this.props.onSelect(dataset);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<td>{dataset.name.split('/')[5]}</td>
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
onHighlightItem(dataset) {
|
|
||||||
this.setState({ highlightedItem: dataset });
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { loading, datasets, filter, error } = this.props;
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
return <p>{error}</p>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const loadingIcon = (
|
|
||||||
<Icon name="circle-notch" className="loading-icon-spin loading-icon" />
|
|
||||||
);
|
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return loadingIcon;
|
|
||||||
}
|
|
||||||
|
|
||||||
const body = (
|
|
||||||
<tbody id="DatasetList">
|
|
||||||
{datasets.filter(dataset => (dataset.name.split('/')[5].toLowerCase().includes(filter.toLowerCase()) || filter=="")).map(this.renderTableRow)}
|
|
||||||
</tbody>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<table id="tblDatasetList" className="gcp-table table noselect">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>{this.props.t('Dataset')}</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
{datasets && body}
|
|
||||||
</table>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default withTranslation('Common')(DatasetsList);
|
|
||||||
@ -1,49 +0,0 @@
|
|||||||
import React, { Component } from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import Modal from 'react-bootstrap-modal';
|
|
||||||
import DicomUploader from './DicomUploader';
|
|
||||||
import { withTranslation } from 'react-i18next';
|
|
||||||
|
|
||||||
class DicomFileUploaderModal extends Component {
|
|
||||||
static propTypes = {
|
|
||||||
url: PropTypes.string,
|
|
||||||
retrieveAuthHeaderFunction: PropTypes.func,
|
|
||||||
onClose: PropTypes.func,
|
|
||||||
};
|
|
||||||
|
|
||||||
state = {
|
|
||||||
uploaded: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
if (!this.props.url) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
show={this.props.isOpen}
|
|
||||||
onHide={this.props.onClose}
|
|
||||||
aria-labelledby="ModalHeader"
|
|
||||||
className="modal fade themed in"
|
|
||||||
backdrop={false}
|
|
||||||
size={'md'}
|
|
||||||
keyboard={true}
|
|
||||||
>
|
|
||||||
<Modal.Header closeButton>
|
|
||||||
<Modal.Title>
|
|
||||||
{this.props.t('Upload DICOM Files')}
|
|
||||||
</Modal.Title>
|
|
||||||
</Modal.Header>
|
|
||||||
<Modal.Body>
|
|
||||||
<DicomUploader
|
|
||||||
url={this.props.url}
|
|
||||||
retrieveAuthHeaderFunction={this.props.retrieveAuthHeaderFunction}
|
|
||||||
/>
|
|
||||||
</Modal.Body>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default withTranslation('Common')(DicomFileUploaderModal);
|
|
||||||
@ -1,82 +0,0 @@
|
|||||||
import React, { Component } from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import './googleCloud.css';
|
|
||||||
import { withTranslation } from 'react-i18next';
|
|
||||||
import { Icon } from '@ohif/ui';
|
|
||||||
|
|
||||||
class DicomStoreList extends Component {
|
|
||||||
state = {
|
|
||||||
search: '',
|
|
||||||
};
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
stores: PropTypes.array,
|
|
||||||
loading: PropTypes.bool.isRequired,
|
|
||||||
error: PropTypes.string,
|
|
||||||
onSelect: PropTypes.func,
|
|
||||||
};
|
|
||||||
|
|
||||||
static defaultProps = {
|
|
||||||
loading: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
renderTableRow = store => {
|
|
||||||
return (
|
|
||||||
<tr
|
|
||||||
key={store.name}
|
|
||||||
className={
|
|
||||||
this.state.highlightedItem === store.name
|
|
||||||
? 'noselect active'
|
|
||||||
: 'noselect'
|
|
||||||
}
|
|
||||||
onMouseEnter={() => {
|
|
||||||
this.onHighlightItem(store.name);
|
|
||||||
}}
|
|
||||||
onClick={() => {
|
|
||||||
this.props.onSelect(store);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<td className="project">{store.name.split('/')[7]}</td>
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
onHighlightItem(store) {
|
|
||||||
this.setState({ highlightedItem: store });
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { loading, stores, filter, error } = this.props;
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
return <p>{error}</p>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const loadingIcon = (
|
|
||||||
<Icon name="circle-notch" className="loading-icon-spin loading-icon" />
|
|
||||||
);
|
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return loadingIcon;
|
|
||||||
}
|
|
||||||
|
|
||||||
const body = (
|
|
||||||
<tbody id="StoreList">{
|
|
||||||
stores.filter(store => (store.name.split('/')[7].toLowerCase().includes(filter.toLowerCase()) || filter=="")).map(this.renderTableRow)}
|
|
||||||
</tbody>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<table id="tblStoreList" className="gcp-table table noselect">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>{this.props.t('DICOM Store')}</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
{stores && body}
|
|
||||||
</table>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default withTranslation('Common')(DicomStoreList);
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
.dicom-uploader .button {
|
|
||||||
float: left;
|
|
||||||
margin: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.invisible-input {
|
|
||||||
position: absolute;
|
|
||||||
display: none;
|
|
||||||
z-index: -1000;
|
|
||||||
max-width: 0 !important;
|
|
||||||
max-height: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dicom-uploader .table-header {
|
|
||||||
color: #ffffff;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 28px;
|
|
||||||
text-align: left;
|
|
||||||
margin: 20px auto;
|
|
||||||
}
|
|
||||||
@ -1,188 +0,0 @@
|
|||||||
import React, { Component } from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { formatFileSize } from './utils/helpers';
|
|
||||||
import CancellationToken from './utils/CancellationToken';
|
|
||||||
import dicomUploader from './api/DicomUploadService';
|
|
||||||
import './DicomUploader.css';
|
|
||||||
|
|
||||||
export default class DicomUploader extends Component {
|
|
||||||
state = {
|
|
||||||
status: 'Upload',
|
|
||||||
isCancelled: false,
|
|
||||||
errorsCount: 0,
|
|
||||||
files: null,
|
|
||||||
uploadedVolume: null,
|
|
||||||
wholeVolumeStr: null,
|
|
||||||
isFilesListHidden: true,
|
|
||||||
timeLeft: null,
|
|
||||||
uploadedList: null,
|
|
||||||
totalCount: 0,
|
|
||||||
successfullyUploadedCount: 0,
|
|
||||||
lastFile: '',
|
|
||||||
uploadContext: null, // this is probably not needed, but we use this variable to distinguish between different downloads
|
|
||||||
};
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
id: PropTypes.string,
|
|
||||||
event: PropTypes.string,
|
|
||||||
url: PropTypes.string,
|
|
||||||
retrieveAuthHeaderFunction: PropTypes.func,
|
|
||||||
};
|
|
||||||
|
|
||||||
filesLeft() {
|
|
||||||
return (
|
|
||||||
this.state.uploadedList.length + ' of ' + this.state.totalCount + ' files'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
volumeLeft() {
|
|
||||||
let left = formatFileSize(this.state.uploadedVolume);
|
|
||||||
return left + ' of ' + this.state.wholeVolumeStr;
|
|
||||||
}
|
|
||||||
|
|
||||||
percents() {
|
|
||||||
return parseInt(
|
|
||||||
(100 * this.state.uploadedList.length) /
|
|
||||||
Object.keys(this.state.files).length
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
isFinished() {
|
|
||||||
return (
|
|
||||||
this.state.isCancelled ||
|
|
||||||
Object.keys(this.state.files).length === this.state.uploadedList.length
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
errorsMessage() {
|
|
||||||
const errors = this.state.errorsCount === 1 ? ' error' : ' errors';
|
|
||||||
return (
|
|
||||||
this.state.errorsCount + errors + ' while uploading, click for more info'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
uploadFiles = files => {
|
|
||||||
const filesArray = Array.from(files.target.files);
|
|
||||||
const filesDict = {};
|
|
||||||
filesArray.forEach((file, i) => {
|
|
||||||
const fileDesc = {
|
|
||||||
id: i,
|
|
||||||
name: file.name,
|
|
||||||
path: file.webkitRelativePath || file.name,
|
|
||||||
size: file.size,
|
|
||||||
error: null,
|
|
||||||
processed: false,
|
|
||||||
processedInUI: false,
|
|
||||||
};
|
|
||||||
filesDict[i] = fileDesc;
|
|
||||||
file.fileId = i;
|
|
||||||
});
|
|
||||||
const wholeVolume = filesArray.map(f => f.size).reduce((a, b) => a + b);
|
|
||||||
const uploadContext = Math.random();
|
|
||||||
this.setState({
|
|
||||||
status: 'Uploading...',
|
|
||||||
files: filesDict,
|
|
||||||
uploadedList: [],
|
|
||||||
uploadedVolume: 0,
|
|
||||||
lastFile: filesArray[0].name,
|
|
||||||
totalCount: filesArray.length,
|
|
||||||
wholeVolumeStr: formatFileSize(wholeVolume),
|
|
||||||
uploadContext: uploadContext,
|
|
||||||
cancellationToken: new CancellationToken(),
|
|
||||||
});
|
|
||||||
const cancellationToken = new CancellationToken();
|
|
||||||
const uploadCallback = (fileId, error) =>
|
|
||||||
uploadContext === this.state.uploadContext &&
|
|
||||||
this.uploadCallback.call(this, fileId, error);
|
|
||||||
|
|
||||||
dicomUploader.setRetrieveAuthHeaderFunction(this.props.retrieveAuthHeaderFunction);
|
|
||||||
|
|
||||||
dicomUploader.smartUpload(
|
|
||||||
files.target.files,
|
|
||||||
this.props.url,
|
|
||||||
uploadCallback,
|
|
||||||
cancellationToken
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
uploadCallback(fileId, error) {
|
|
||||||
const file = this.state.files[fileId];
|
|
||||||
file.processed = true;
|
|
||||||
if (!error) {
|
|
||||||
let uploadedVolume = this.state.uploadedVolume + file.size;
|
|
||||||
this.setState({ uploadedVolume });
|
|
||||||
} else {
|
|
||||||
file.error = error;
|
|
||||||
this.setState({ errorsCount: this.state.errorsCount + 1 });
|
|
||||||
}
|
|
||||||
this.setState({ lastFile: file.name });
|
|
||||||
let uploadedList = this.state.uploadedList;
|
|
||||||
uploadedList.push(file);
|
|
||||||
this.setState({ uploadedList });
|
|
||||||
}
|
|
||||||
|
|
||||||
renderTableRow = file => {
|
|
||||||
let error = null;
|
|
||||||
if (file.error !== null) {
|
|
||||||
error = <p style={{ color: 'red' }}>{file.error}</p>;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<tr key={file.id}>
|
|
||||||
<td className="project">
|
|
||||||
{file.name} {error}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
if (this.state.files === null) {
|
|
||||||
return (
|
|
||||||
<div className="dicom-uploader">
|
|
||||||
<div className="button">
|
|
||||||
<label htmlFor="file">
|
|
||||||
<img src="./assets/Button_File.svg" alt="upload file"></img>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
className="invisible-input"
|
|
||||||
onChange={this.uploadFiles}
|
|
||||||
type="file"
|
|
||||||
id="file"
|
|
||||||
multiple
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="button">
|
|
||||||
<label htmlFor="folder">
|
|
||||||
<img src="./assets/Button_Folder.svg" alt="upload folder"></img>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
className="invisible-input"
|
|
||||||
type="file"
|
|
||||||
onChange={this.uploadFiles}
|
|
||||||
id="folder"
|
|
||||||
webkitdirectory="true"
|
|
||||||
mozdirectory="true"
|
|
||||||
multiple
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<table id="tblProjectList" className="table noselect">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th className="table-header">
|
|
||||||
{this.percents()}% {this.filesLeft()}
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody id="ProjectList">
|
|
||||||
{this.state.uploadedList.map(this.renderTableRow)}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,82 +0,0 @@
|
|||||||
import React, { Component } from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import './googleCloud.css';
|
|
||||||
import { withTranslation } from 'react-i18next';
|
|
||||||
import { Icon } from '@ohif/ui';
|
|
||||||
|
|
||||||
class LocationsList extends Component {
|
|
||||||
state = {
|
|
||||||
search: '',
|
|
||||||
};
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
locations: PropTypes.array,
|
|
||||||
loading: PropTypes.bool.isRequired,
|
|
||||||
error: PropTypes.string,
|
|
||||||
onSelect: PropTypes.func,
|
|
||||||
};
|
|
||||||
|
|
||||||
static defaultProps = {
|
|
||||||
loading: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
renderTableRow = location => {
|
|
||||||
return (
|
|
||||||
<tr
|
|
||||||
key={location.locationId}
|
|
||||||
className={
|
|
||||||
this.state.highlightedItem === location.locationId
|
|
||||||
? 'noselect active'
|
|
||||||
: 'noselect'
|
|
||||||
}
|
|
||||||
onMouseEnter={() => {
|
|
||||||
this.onHighlightItem(location.locationId);
|
|
||||||
}}
|
|
||||||
onClick={() => {
|
|
||||||
this.props.onSelect(location);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<td>{location.name.split('/')[3]}</td>
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
onHighlightItem(locationId) {
|
|
||||||
this.setState({ highlightedItem: locationId });
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { loading, locations, filter, error } = this.props;
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
return <p>{error}</p>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const loadingIcon = (
|
|
||||||
<Icon name="circle-notch" className="loading-icon-spin loading-icon" />
|
|
||||||
);
|
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return loadingIcon;
|
|
||||||
}
|
|
||||||
|
|
||||||
const body = (
|
|
||||||
<tbody id="LocationList">
|
|
||||||
{locations.filter(location => (location.name.split('/')[3].toLowerCase().includes(filter.toLowerCase()) || filter=="")).map(this.renderTableRow)}
|
|
||||||
</tbody>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<table id="tblLocationList" className="gcp-table table noselect">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>{this.props.t('Location')}</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
{locations && body}
|
|
||||||
</table>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default withTranslation('Common')(LocationsList);
|
|
||||||
@ -1,95 +0,0 @@
|
|||||||
import React, { Component } from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import './googleCloud.css';
|
|
||||||
import { withTranslation } from 'react-i18next';
|
|
||||||
import { Icon } from '@ohif/ui';
|
|
||||||
|
|
||||||
class ProjectsList extends Component {
|
|
||||||
state = {
|
|
||||||
search: '',
|
|
||||||
highlightedItem: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
projects: PropTypes.array,
|
|
||||||
loading: PropTypes.bool.isRequired,
|
|
||||||
error: PropTypes.string,
|
|
||||||
onSelect: PropTypes.func.isRequired,
|
|
||||||
t: PropTypes.func,
|
|
||||||
};
|
|
||||||
|
|
||||||
static defaultProps = {
|
|
||||||
loading: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
renderTableRow = project => {
|
|
||||||
return (
|
|
||||||
<tr
|
|
||||||
key={project.projectId}
|
|
||||||
className={
|
|
||||||
this.state.highlightedItem === project.projectId
|
|
||||||
? 'noselect active'
|
|
||||||
: 'noselect'
|
|
||||||
}
|
|
||||||
onMouseEnter={() => {
|
|
||||||
this.onHighlightItem(project.projectId);
|
|
||||||
}}
|
|
||||||
onClick={() => {
|
|
||||||
this.onHighlightItem(project.projectId);
|
|
||||||
this.props.onSelect(project);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<td>{project.name}</td>
|
|
||||||
<td>{project.projectId}</td>
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
onHighlightItem(project) {
|
|
||||||
this.setState({ highlightedItem: project });
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { loading, projects, filter, error } = this.props;
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
return <p>{error}</p>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const loadingIcon = (
|
|
||||||
<Icon name="circle-notch" className="loading-icon-spin loading-icon" />
|
|
||||||
);
|
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return loadingIcon;
|
|
||||||
}
|
|
||||||
|
|
||||||
const lowerCaseFilter = filter.toLowerCase();
|
|
||||||
const filteredProjects = projects.filter(project =>
|
|
||||||
typeof project.name === 'string' &&
|
|
||||||
(filter === "" || project.name.toLowerCase().includes(lowerCaseFilter))
|
|
||||||
);
|
|
||||||
|
|
||||||
const body = (
|
|
||||||
<tbody id="ProjectList">
|
|
||||||
{
|
|
||||||
filteredProjects.map(this.renderTableRow)
|
|
||||||
}
|
|
||||||
</tbody>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<table id="tblProjectList" className="gcp-table table noselect">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>{this.props.t('Project')}</th>
|
|
||||||
<th>{this.props.t('ID')}</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
{projects && body}
|
|
||||||
</table>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default withTranslation('Common')(ProjectsList);
|
|
||||||
@ -1,109 +0,0 @@
|
|||||||
class GoogleCloudApi {
|
|
||||||
setAccessToken(accessToken) {
|
|
||||||
if (!accessToken) console.error('Access token is empty');
|
|
||||||
this.accessToken = accessToken;
|
|
||||||
}
|
|
||||||
|
|
||||||
get fetchConfig() {
|
|
||||||
if (!this.accessToken) throw new Error('OIDC access_token is not set');
|
|
||||||
return {
|
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
Authorization: 'Bearer ' + this.accessToken,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
get urlBase() {
|
|
||||||
return this.healthcareApiEndpoint || 'https://healthcare.googleapis.com/v1beta1';
|
|
||||||
}
|
|
||||||
|
|
||||||
set urlBase(url) {
|
|
||||||
this.healthcareApiEndpoint = url;
|
|
||||||
}
|
|
||||||
|
|
||||||
get urlBaseProject() {
|
|
||||||
return this.urlBase + `/projects`;
|
|
||||||
}
|
|
||||||
|
|
||||||
getUrlBaseDicomWeb(project, location, dataset, dicomStore) {
|
|
||||||
return (
|
|
||||||
this.urlBase +
|
|
||||||
`/projects/${project}/locations/${location}/datasets/${dataset}/dicomStores/${dicomStore}/dicomWeb`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
getUrlPath(project, location, dataset, dicomStore) {
|
|
||||||
`/projects/${project}/locations/${location}/datasets/${dataset}/dicomStores/${dicomStore}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
async doRequest(urlStr, config = {}, params = {}) {
|
|
||||||
const url = new URL(urlStr);
|
|
||||||
let data = null;
|
|
||||||
url.search = new URLSearchParams(params);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(url, { ...this.fetchConfig, config });
|
|
||||||
try {
|
|
||||||
data = await response.json();
|
|
||||||
} catch (err) {}
|
|
||||||
if (response.status >= 200 && response.status < 300 && data != null) {
|
|
||||||
if (data.nextPageToken != null) {
|
|
||||||
params.pageToken = data.nextPageToken;
|
|
||||||
let subPage = await this.doRequest(urlStr, config, params);
|
|
||||||
for (let key in data) {
|
|
||||||
if (data.hasOwnProperty(key)) {
|
|
||||||
data[key] = data[key].concat(subPage.data[key]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
isError: false,
|
|
||||||
status: response.status,
|
|
||||||
data,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
return {
|
|
||||||
isError: true,
|
|
||||||
status: response.status,
|
|
||||||
message:
|
|
||||||
(data && data.error && data.error.message) || 'Unknown error',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
if (data && data.error) {
|
|
||||||
return {
|
|
||||||
isError: true,
|
|
||||||
status: err.status,
|
|
||||||
message: err.response.data.error.message || 'Unspecified error',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
isError: true,
|
|
||||||
message: (err && err.message) || 'Oops! Something went wrong',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async loadProjects() {
|
|
||||||
return this.doRequest(
|
|
||||||
'https://cloudresourcemanager.googleapis.com/v1/projects'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async loadLocations(projectId) {
|
|
||||||
return this.doRequest(`${this.urlBaseProject}/${projectId}/locations`);
|
|
||||||
}
|
|
||||||
|
|
||||||
async loadDatasets(projectId, locationId) {
|
|
||||||
return this.doRequest(
|
|
||||||
`${this.urlBaseProject}/${projectId}/locations/${locationId}/datasets`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async loadDicomStores(dataset) {
|
|
||||||
return this.doRequest(`${this.urlBase}/${dataset}/dicomStores`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default new GoogleCloudApi();
|
|
||||||
@ -1,155 +0,0 @@
|
|||||||
.loading-icon-spin {
|
|
||||||
animation: spin 2s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading-icon {
|
|
||||||
font-size: 30px;
|
|
||||||
margin: 20px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin {
|
|
||||||
100% {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.gcp-table {
|
|
||||||
width: 100%;
|
|
||||||
max-width: 100%;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
border-spacing: 0;
|
|
||||||
border-collapse: collapse;
|
|
||||||
}
|
|
||||||
.gcp-table > tr {
|
|
||||||
height: 20px;
|
|
||||||
}
|
|
||||||
.gcp-table > thead {
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
.gcp-table > thead tr th {
|
|
||||||
padding: 0;
|
|
||||||
border-bottom: 1px solid var(--ui-border-color-active);
|
|
||||||
width: 100%;
|
|
||||||
text-align: left;
|
|
||||||
border-top: 0;
|
|
||||||
}
|
|
||||||
.gcp-table > tbody tr {
|
|
||||||
padding: 5px;
|
|
||||||
background-color: var(--ui-gray-darker);
|
|
||||||
}
|
|
||||||
.gcp-table > tbody tr:nth-child(even) {
|
|
||||||
background-color: var(--ui-gray-dark);
|
|
||||||
}
|
|
||||||
|
|
||||||
.gcp-table > tbody tr td {
|
|
||||||
padding: 8px;
|
|
||||||
height: $body-cell-height;
|
|
||||||
line-height: $body-cell-height;
|
|
||||||
color: var(--table-text-primary-color);
|
|
||||||
font-weight: 300;
|
|
||||||
border-top: 1px solid var(--ui-gray-lighter);
|
|
||||||
border-bottom: 1px solid var(--ui-gray-lighter);
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
.gcp-table > tbody tr td.emptyCell {
|
|
||||||
color: #516873;
|
|
||||||
}
|
|
||||||
.gcp-table > tbody tr td.emptyValue {
|
|
||||||
color: var(--ui-gray-light);
|
|
||||||
}
|
|
||||||
.gcp-table > tbody tr:hover,
|
|
||||||
.gcp-table > tbody tr:active,
|
|
||||||
.gcp-table > tbody tr.active {
|
|
||||||
background-color: var(--table-hover-color);
|
|
||||||
color: var(--text-primary-color);
|
|
||||||
}
|
|
||||||
.gcp-table > tbody tr:hover td,
|
|
||||||
.gcp-table > tbody tr:active td,
|
|
||||||
.gcp-table > tbody tr.active td {
|
|
||||||
border-top: 1px solid var(--ui-gray-lighter);
|
|
||||||
border-bottom: 1px solid var(--ui-gray-lighter);
|
|
||||||
background-color: var(--table-hover-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.gcp-files-selector {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
.gcp-files-selector__btn-dir,
|
|
||||||
.gcp-files-selector__btn-file {
|
|
||||||
height: 80px !important;
|
|
||||||
width: 228px !important;
|
|
||||||
font-size: 14px;
|
|
||||||
border-radius: 8px;
|
|
||||||
margin: 0 !important;
|
|
||||||
}
|
|
||||||
.gcp-files-selector__btn-dir {
|
|
||||||
background-image: url('/assets/Button_Folder.svg');
|
|
||||||
}
|
|
||||||
.gcp-files-selector__btn-file {
|
|
||||||
background-image: url('/assets/Button_File.svg');
|
|
||||||
}
|
|
||||||
|
|
||||||
.gcp-window {
|
|
||||||
background-color: #000000;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gcp-hidden {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gcp-picker {
|
|
||||||
box-sizing: border-box;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
width: 650px;
|
|
||||||
height: 600px;
|
|
||||||
background-color: #161a1f;
|
|
||||||
padding: 20px 30px;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gcp-dicom-picker__exit {
|
|
||||||
cursor: pointer;
|
|
||||||
position: absolute;
|
|
||||||
top: 30px;
|
|
||||||
right: 30px;
|
|
||||||
width: 14px !important;
|
|
||||||
height: 14px !important;
|
|
||||||
filter: opacity(0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.gcp-dicom-picker__exit:hover {
|
|
||||||
filter: opacity(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.gcp-picker--btn {
|
|
||||||
margin: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gcp-picker--title {
|
|
||||||
color: #ffffff;
|
|
||||||
font-size: 24px;
|
|
||||||
text-align: center;
|
|
||||||
margin: 20px auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gcp-picker--path {
|
|
||||||
color: #ffffff;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 28px;
|
|
||||||
text-align: left;
|
|
||||||
margin: 20px auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gcp-input{
|
|
||||||
height: 40px;
|
|
||||||
margin: 0 5px 20px 5px;
|
|
||||||
padding: 0 20px;
|
|
||||||
cursor: pointer;
|
|
||||||
border: none;
|
|
||||||
background-color: var(--input-background-color);
|
|
||||||
color: var(--input-placeholder-color);
|
|
||||||
font-size: 10pt;
|
|
||||||
font-weight: normal;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
@ -1,13 +0,0 @@
|
|||||||
export default class CancellationToken {
|
|
||||||
constructor() {
|
|
||||||
this.cancelled = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
get() {
|
|
||||||
return this.cancelled;
|
|
||||||
}
|
|
||||||
|
|
||||||
set(value) {
|
|
||||||
this.cancelled = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,59 +0,0 @@
|
|||||||
const getServers = (data, name) => {
|
|
||||||
const {
|
|
||||||
wadoUriRoot,
|
|
||||||
qidoRoot,
|
|
||||||
wadoRoot,
|
|
||||||
dataset = '',
|
|
||||||
dicomStore = '',
|
|
||||||
location = '',
|
|
||||||
project = '',
|
|
||||||
} = data;
|
|
||||||
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
name: name,
|
|
||||||
dataset,
|
|
||||||
dicomStore,
|
|
||||||
location,
|
|
||||||
project,
|
|
||||||
imageRendering: 'wadors',
|
|
||||||
thumbnailRendering: 'wadors',
|
|
||||||
type: 'dicomWeb',
|
|
||||||
active: true,
|
|
||||||
wadoUriRoot,
|
|
||||||
qidoRoot,
|
|
||||||
wadoRoot,
|
|
||||||
supportsFuzzyMatching: false,
|
|
||||||
qidoSupportsIncludeField: false,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
const isValidServer = server => {
|
|
||||||
return (
|
|
||||||
server &&
|
|
||||||
!!server.dataset &&
|
|
||||||
!!server.dicomStore &&
|
|
||||||
!!server.location &&
|
|
||||||
!!server.project
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const isEqualServer = (server = {}, toCompare = {}) => {
|
|
||||||
const serverLength = Object.keys(server).length;
|
|
||||||
const toCompareLength = Object.keys(toCompare).length;
|
|
||||||
|
|
||||||
if (!serverLength || !toCompareLength) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
server.dataset === toCompare.dataset &&
|
|
||||||
server.dataset === toCompare.dataset &&
|
|
||||||
server.dicomStore === toCompare.dicomStore &&
|
|
||||||
server.location === toCompare.location &&
|
|
||||||
server.project === toCompare.project
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export { getServers, isValidServer, isEqualServer };
|
|
||||||
@ -1,35 +0,0 @@
|
|||||||
export function formatFileSize(size) {
|
|
||||||
if (size === 0) return '0 B';
|
|
||||||
const n = Math.floor(Math.log(size) / Math.log(1024));
|
|
||||||
return (
|
|
||||||
(size / Math.pow(1024, n)).toFixed(2) * 1 +
|
|
||||||
' ' +
|
|
||||||
['B', 'kB', 'MB', 'GB', 'TB'][n]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function httpErrorToStr(error) {
|
|
||||||
if (!error) return 'Unknown error';
|
|
||||||
if (error.response) {
|
|
||||||
try {
|
|
||||||
const jsonResponse = JSON.parse(error.response);
|
|
||||||
if (
|
|
||||||
jsonResponse.error &&
|
|
||||||
jsonResponse.error.code &&
|
|
||||||
jsonResponse.error.message
|
|
||||||
)
|
|
||||||
return jsonResponse.error.code + ' - ' + jsonResponse.error.message;
|
|
||||||
} catch (err) {
|
|
||||||
return error.response;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return error.message || 'Unknown error.';
|
|
||||||
}
|
|
||||||
|
|
||||||
/* eslint-disable */
|
|
||||||
export function checkDicomFile(arrayBuffer) {
|
|
||||||
if (arrayBuffer.length <= 132) return false;
|
|
||||||
const arr = new Uint8Array(arrayBuffer.slice(128, 132));
|
|
||||||
// bytes from 128 to 132 must be "DICM"
|
|
||||||
return Array.from('DICM').every((char, i) => char.charCodeAt(0) === arr[i]);
|
|
||||||
}
|
|
||||||
@ -1,15 +0,0 @@
|
|||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<title>OHIF Viewer</title>
|
|
||||||
<meta name="description" content="Open Health Imaging Foundation DICOM Viewer">
|
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1.0,minimum-scale=1,maximum-scale=1,user-scalable=no">
|
|
||||||
<link rel="shortcut icon" type="image/png" href="/images/logo.png" sizes="16x16 32x32 64x64">
|
|
||||||
<!--Best display for mobile devices-->
|
|
||||||
<meta http-equiv="cleartype" content="on">
|
|
||||||
<meta name="MobileOptimized" content="320">
|
|
||||||
<meta name="HandheldFriendly" content="True">
|
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="root"></div>
|
|
||||||
</body>
|
|
||||||
@ -2,49 +2,16 @@
|
|||||||
* Entry point for development and production PWA builds.
|
* Entry point for development and production PWA builds.
|
||||||
* Packaged (NPM) builds go through `index-umd.js`
|
* Packaged (NPM) builds go through `index-umd.js`
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import 'regenerator-runtime/runtime';
|
import 'regenerator-runtime/runtime';
|
||||||
|
|
||||||
import App from './App.js';
|
import App from './App.js';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
// test
|
// test
|
||||||
|
|
||||||
/**
|
/** Combine our appConfiguration and "baked-in" extensions */
|
||||||
* EXTENSIONS
|
|
||||||
* =================
|
|
||||||
*
|
|
||||||
* Importing and modifying the extensions our app uses HERE allows us to leverage
|
|
||||||
* tree shaking and a few other niceties. However, by including them here they become
|
|
||||||
* "baked in" to the published application.
|
|
||||||
*
|
|
||||||
* Depending on your use case/needs, you may want to consider not adding any extensions
|
|
||||||
* by default HERE, and instead provide them via the extensions configuration key or
|
|
||||||
* by using the exported `App` component, and passing in your extensions as props using
|
|
||||||
* the defaultExtensions property.
|
|
||||||
*/
|
|
||||||
// import OHIFVTKExtension from '@ohif/extension-vtk';
|
|
||||||
// import OHIFDicomHtmlExtension from '@ohif/extension-dicom-html';
|
|
||||||
// import OHIFDicomMicroscopyExtension from '@ohif/extension-dicom-microscopy';
|
|
||||||
// import OHIFDicomPDFExtension from '@ohif/extension-dicom-pdf';
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Default Settings
|
|
||||||
*/
|
|
||||||
let config = {};
|
|
||||||
|
|
||||||
if (window) {
|
|
||||||
config = window.config || {};
|
|
||||||
}
|
|
||||||
|
|
||||||
const appProps = {
|
const appProps = {
|
||||||
config,
|
config: window ? window.config : {},
|
||||||
// defaultExtensions: [
|
defaultExtensions: [],
|
||||||
// OHIFVTKExtension,
|
|
||||||
// OHIFDicomHtmlExtension,
|
|
||||||
// OHIFDicomMicroscopyExtension,
|
|
||||||
// OHIFDicomPDFExtension,
|
|
||||||
// ],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Create App */
|
/** Create App */
|
||||||
|
|||||||
@ -1,26 +0,0 @@
|
|||||||
import FileLoaderService from './localFileLoaders/fileLoaderService';
|
|
||||||
|
|
||||||
const processFile = async file => {
|
|
||||||
try {
|
|
||||||
const fileLoaderService = new FileLoaderService(file);
|
|
||||||
const imageId = fileLoaderService.addFile(file);
|
|
||||||
const image = await fileLoaderService.loadFile(file, imageId);
|
|
||||||
const dataset = await fileLoaderService.getDataset(image, imageId);
|
|
||||||
const studies = await fileLoaderService.getStudies(dataset, imageId);
|
|
||||||
|
|
||||||
return studies;
|
|
||||||
} catch (error) {
|
|
||||||
console.log(
|
|
||||||
error.name,
|
|
||||||
':Error when trying to load and process local files:',
|
|
||||||
error.message
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default async function filesToStudies(files) {
|
|
||||||
const processFilesPromises = files.map(processFile);
|
|
||||||
const studies = await Promise.all(processFilesPromises);
|
|
||||||
|
|
||||||
return FileLoaderService.groupSeries(studies.flat());
|
|
||||||
}
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
export default class FileLoader {
|
|
||||||
fileType;
|
|
||||||
loadFile(file, imageId) { }
|
|
||||||
getDataset(image, imageId) { }
|
|
||||||
getStudies(dataset, imageId) { }
|
|
||||||
}
|
|
||||||
@ -1,46 +0,0 @@
|
|||||||
export default function bounding(elementRef, currentPosition = {}) {
|
|
||||||
if (!elementRef.current) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentElement = elementRef.current;
|
|
||||||
const {
|
|
||||||
offsetParent,
|
|
||||||
offsetTop,
|
|
||||||
offsetHeight,
|
|
||||||
offsetLeft,
|
|
||||||
offsetWidth,
|
|
||||||
} = currentElement;
|
|
||||||
let top = currentPosition.top || offsetTop;
|
|
||||||
let left = currentPosition.left || offsetLeft;
|
|
||||||
|
|
||||||
if (!offsetParent) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let maxHeight = `${offsetParent.offsetHeight}px`;
|
|
||||||
|
|
||||||
if (offsetHeight + top > offsetParent.offsetHeight) {
|
|
||||||
top = top - (offsetHeight + top - offsetParent.offsetHeight);
|
|
||||||
if (top < 0) {
|
|
||||||
top = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (left + offsetWidth > offsetParent.offsetWidth) {
|
|
||||||
left = offsetParent.offsetWidth - offsetWidth;
|
|
||||||
if (left < 0) {
|
|
||||||
left = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (maxHeight && currentElement.style.maxHeight !== maxHeight) {
|
|
||||||
currentElement.style.maxHeight = maxHeight;
|
|
||||||
}
|
|
||||||
if (currentElement.style.top !== `${top}px`) {
|
|
||||||
currentElement.style.top = `${top}px`;
|
|
||||||
}
|
|
||||||
if (currentElement.style.left !== `${left}px`) {
|
|
||||||
currentElement.style.left = `${left}px`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,32 +0,0 @@
|
|||||||
import React, { Component } from 'react';
|
|
||||||
import { withRouter } from 'react-router-dom';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { CallbackComponent } from 'redux-oidc';
|
|
||||||
|
|
||||||
class CallbackPage extends Component {
|
|
||||||
static propTypes = {
|
|
||||||
userManager: PropTypes.object.isRequired,
|
|
||||||
history: PropTypes.object.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<CallbackComponent
|
|
||||||
userManager={this.props.userManager}
|
|
||||||
successCallback={() => {
|
|
||||||
const pathname = sessionStorage.getItem('ohif-redirect-to');
|
|
||||||
|
|
||||||
this.props.history.push(pathname);
|
|
||||||
}}
|
|
||||||
errorCallback={error => {
|
|
||||||
//this.props.history.push("/");
|
|
||||||
throw new Error(error);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div>Redirecting...</div>
|
|
||||||
</CallbackComponent>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default withRouter(CallbackPage);
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
.not-found {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
@ -1,18 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import './NotFound.css';
|
|
||||||
import { Link } from 'react-router-dom';
|
|
||||||
|
|
||||||
export default function NotFound({ message = 'Sorry, this page does not exist.', showGoBackButton = true }) {
|
|
||||||
return (
|
|
||||||
<div className={'not-found'}>
|
|
||||||
<div>
|
|
||||||
<h4>{message}</h4>
|
|
||||||
{showGoBackButton && (
|
|
||||||
<h5>
|
|
||||||
<Link to={'/'}>Go back to the Study List</Link>
|
|
||||||
</h5>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
28
platform/viewer/src/routes/NotFound/NotFound.jsx
Normal file
28
platform/viewer/src/routes/NotFound/NotFound.jsx
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
const NotFound = ({
|
||||||
|
message = 'Sorry, this page does not exist.',
|
||||||
|
showGoBackButton = true,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div className="w-full h-full flex justify-center items-center text-white">
|
||||||
|
<div>
|
||||||
|
<h4>{message}</h4>
|
||||||
|
{showGoBackButton && (
|
||||||
|
<h5>
|
||||||
|
<Link to={'/'}>Go back to the Study List</Link>
|
||||||
|
</h5>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
NotFound.propTypes = {
|
||||||
|
message: PropTypes.string,
|
||||||
|
showGoBackButton: PropTypes.bool,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NotFound;
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user