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:
Rodrigo Antinarellianddannyrb authored and James A. Petts committed 2020-05-15 13:57:23 +02:00
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 };