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:
1 parent
9a8bede80e
commit
01b314522d
116 files changed
+22080
-4971
No files matched your search
Whitespace-only changes.
@@ -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 };
|
||||
Reference in new issue
Block a user