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
|
||||
|
||||
# Build && Move PWA Output
|
||||
# yarn run build:ci
|
||||
# mkdir -p ./.netlify/www/pwa
|
||||
# mv platform/viewer/dist/* .netlify/www/pwa -v
|
||||
yarn run build:ci
|
||||
mkdir -p ./.netlify/www/pwa
|
||||
mv platform/viewer/dist/* .netlify/www/pwa -v
|
||||
|
||||
# Build && Move script output
|
||||
# yarn run build:package
|
||||
@ -25,6 +25,3 @@ mkdir -p ./.netlify/www/ui
|
||||
mv platform/ui/.docz/dist/* .netlify/www/ui -v
|
||||
|
||||
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) => {
|
||||
if (Array.isArray(childComponents)) {
|
||||
return childComponents.find(
|
||||
(_childComponent) => _childComponent.name === componentOpen
|
||||
_childComponent => _childComponent.name === componentOpen
|
||||
);
|
||||
} else {
|
||||
return childComponents;
|
||||
@ -80,10 +80,10 @@ const SidePanel = ({
|
||||
const _childComponents = Array.isArray(childComponents)
|
||||
? childComponents
|
||||
: [childComponents];
|
||||
return _childComponents.map((childComponent) => {
|
||||
return _childComponents.map((childComponent, i) => {
|
||||
return (
|
||||
<Button
|
||||
key={childComponent.name}
|
||||
key={i}
|
||||
variant="text"
|
||||
color="inherit"
|
||||
onClick={() => {
|
||||
|
||||
@ -49,25 +49,22 @@ const StudyListPagination = ({
|
||||
</Typography>
|
||||
<ButtonGroup color="primary">
|
||||
<IconButton
|
||||
size="small"
|
||||
size="inherit"
|
||||
className="border-common-active px-4 py-2 text-base"
|
||||
style={{ padding: '3px 12px', minWidth: 0 }}
|
||||
color="white"
|
||||
onClick={() => navigateToPage(1)}
|
||||
>
|
||||
<>{`<<`}</>
|
||||
</IconButton>
|
||||
<Button
|
||||
size="small"
|
||||
className="border-common-active py-2 text-base"
|
||||
style={{ padding: '3px 8px', minWidth: 0 }}
|
||||
size="inherit"
|
||||
className="border-common-active py-2 px-2 text-base"
|
||||
color="white"
|
||||
onClick={() => navigateToPage(currentPage - 1)}
|
||||
>{`< Previous`}</Button>
|
||||
<Button
|
||||
size="small"
|
||||
className="border-common-active py-2 text-base"
|
||||
style={{ padding: '3px 20px', minWidth: 0 }}
|
||||
size="inherit"
|
||||
className="border-common-active py-2 px-4 text-base"
|
||||
color="white"
|
||||
onClick={() => navigateToPage(currentPage + 1)}
|
||||
>
|
||||
|
||||
@ -8,3 +8,4 @@
|
||||
|
||||
PUBLIC_URL=/
|
||||
APP_CONFIG=config/default.js
|
||||
USE_HASH_ROUTER=false
|
||||
|
||||
@ -7,3 +7,4 @@
|
||||
|
||||
PUBLIC_URL=/demo/
|
||||
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 HTML_TEMPLATE = process.env.HTML_TEMPLATE || 'script-tag.html';
|
||||
const PUBLIC_URL = process.env.PUBLIC_URL || '/';
|
||||
const Dotenv = require('dotenv-webpack');
|
||||
|
||||
module.exports = (env, argv) => {
|
||||
const commonConfig = webpackCommon(env, argv, { SRC_DIR, DIST_DIR });
|
||||
@ -34,6 +35,7 @@ module.exports = (env, argv) => {
|
||||
rules: [fontsToJavaScriptRule],
|
||||
},
|
||||
plugins: [
|
||||
new Dotenv(),
|
||||
// Clean output.path
|
||||
new CleanWebpackPlugin(),
|
||||
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_DOMAIN = process.env.PROXY_DOMAIN;
|
||||
const ENTRY_TARGET = process.env.ENTRY_TARGET || `${SRC_DIR}/index.js`;
|
||||
const Dotenv = require('dotenv-webpack');
|
||||
|
||||
module.exports = (env, argv) => {
|
||||
const baseConfig = webpackBase(env, argv, { SRC_DIR, DIST_DIR });
|
||||
@ -46,6 +47,7 @@ module.exports = (env, argv) => {
|
||||
rules: [...extractStyleChunksRule(isProdBuild)],
|
||||
},
|
||||
plugins: [
|
||||
new Dotenv(),
|
||||
// Uncomment to generate bundle analyzer
|
||||
// new BundleAnalyzerPlugin(),
|
||||
// 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",
|
||||
"dicom-parser": "^1.8.3",
|
||||
"dicomweb-client": "^0.4.4",
|
||||
"dotenv-webpack": "^1.7.0",
|
||||
"hammerjs": "^2.0.8",
|
||||
"i18next": "^17.0.3",
|
||||
"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: {
|
||||
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!
|
||||
|
||||
@ -1,14 +1,46 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { BrowserRouter, HashRouter } from 'react-router-dom';
|
||||
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 (
|
||||
<ThemeWrapper>
|
||||
<ConnectedStudyList />
|
||||
</ThemeWrapper>
|
||||
<Router basename={routerBasename}>
|
||||
<ThemeWrapper>{routes()}</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;
|
||||
|
||||
@ -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.
|
||||
* Packaged (NPM) builds go through `index-umd.js`
|
||||
*/
|
||||
|
||||
import 'regenerator-runtime/runtime';
|
||||
|
||||
import App from './App.js';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
// test
|
||||
|
||||
/**
|
||||
* 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 || {};
|
||||
}
|
||||
|
||||
/** Combine our appConfiguration and "baked-in" extensions */
|
||||
const appProps = {
|
||||
config,
|
||||
// defaultExtensions: [
|
||||
// OHIFVTKExtension,
|
||||
// OHIFDicomHtmlExtension,
|
||||
// OHIFDicomMicroscopyExtension,
|
||||
// OHIFDicomPDFExtension,
|
||||
// ],
|
||||
config: window ? window.config : {},
|
||||
defaultExtensions: [],
|
||||
};
|
||||
|
||||
/** 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