fix: Combined Hotkeys for special characters (#1233)
* fix: Combined Hotkeys for special characters * add record method to hotkey manager * fix record plugin * remove unused component * add record to modal props * rename record method * replace handlers to use hotkeyRecord * fix combined keys * change expected result count from 18 to 17 * autoformat * Remove duplicate test, that was testing the wrong things; fix label; update configs * Revert "Remove duplicate test, that was testing the wrong things; fix label; update configs" This reverts commit 4292f4fe67351962d61cae623b920dcbd87dd71d. * Fix the record plugin's registration * fix exposed record method usage * adding logging for info level items * Hotkey definitions don't need to be globally reactive; use localstorage/appconfig as sources of truth; not redux * Tidy up test * Remove unused code from UserPreferencesForm * Log info when we run a command * fix hotkey preference restore * use application configured hotkeys if there are no user preferred * Avoid logging circular ref * Fix callouts * Fix small issue with array * Fix langua issue after refactor and merge * Refactor on recordCurrentCombo as Rodrigo did before * Separating components in 2 files * WIP Refactor to simplify the user preferences and move into each form the save and controll functionalities * Remove context * Remove unused import * Initial work on Field treatment * Refactor General preferences * Small refactor removing type from HotkeyField * small update on style * Refactor and layout fixed * Make hotkeys preferences working with old hotkeys row * Move error handling out of hotkey row/input component * WIP custom form * Moving validation function to component * Exposing hotkeyRecord as it does not depend on HotkeyManager Class * Making hotkeyField as much detached possible from parent component * Small refactors * Refactor on user preferences * Clean up into the changes * Small fix to let save working * Style finish * move about docs into about folder * Fix double tap on single keys * Style refactor * Remove log * Fix log issues on unit tests * Fix unit test breaking on ohif/core index * Fixing hotkeys unpause unit test issue * Rename file to adopt lowercase * Rename file to adopt lowercase * Fixing callouts * Big refactor miving some of the components into viewer and creating small components into ohif/ui * Typo on folder name * Updating ohif ui docs * Remove comments * Fix binding of combo keys * Fix some cypress tests failures * Fixing onCancel button * Fixing e2e tests * Small style update * Fixing unit tests failing after fix issue * Remove some not used code * Remove left over after debug * Adding prevent default on hotkeys events * Fixinf existing hotkeys validator with 3 keys pressed * Exposing hotkeys as root level on ohif-core * Clean up * Exposing all availableLanguages with labels and fixing an issue on language switcher * Fixing e2e cypress tests * Preveinting some simple errors * Treating error once we try to set hotkey definitions * Adding ui notification on setHotkeys errors * Implementing a service queue request to hold until functions are implemented * Making sure toFixed is only called on Numbers Co-authored-by: Danny Brown <danny.ri.brown@gmail.com> Co-authored-by: Gustavo André Lelis <galelis@gmail.com>
This commit is contained in:
69 files changed
+1211
-1632
No files matched your search
File renamed without changes.
@@ -0,0 +1,82 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { hotkeys } from '@ohif/core';
|
||||
|
||||
/**
|
||||
* Take the pressed key array and return the readable string for the keys
|
||||
*
|
||||
* @param {Array} [keys=[]]
|
||||
* @returns {string} string representation of an array of keys
|
||||
*/
|
||||
const formatKeysForInput = (keys = []) => keys.join('+');
|
||||
|
||||
/**
|
||||
* formats given keys sequence to insert the modifier keys in the first index of the array
|
||||
* @param {string} sequence keys sequence from MouseTrap Record -> "shift+left"
|
||||
* @returns {Array} keys in array-format -> ['shift','left']
|
||||
*/
|
||||
const getKeys = ({ sequence, modifier_keys }) => {
|
||||
const keysArray = sequence.join(' ').split('+');
|
||||
let keys = [];
|
||||
let modifiers = [];
|
||||
keysArray.forEach(key => {
|
||||
if (modifier_keys && modifier_keys.includes(key)) {
|
||||
modifiers.push(key);
|
||||
} else {
|
||||
keys.push(key);
|
||||
}
|
||||
});
|
||||
return [...modifiers, ...keys];
|
||||
};
|
||||
|
||||
/**
|
||||
* HotkeyField
|
||||
* Renders a hotkey input
|
||||
*
|
||||
* @param {object} props component props
|
||||
* @param {Array[]} props.keys array of keys to be controlled by this field
|
||||
* @param {function} props.handleChange Callback function to communicate parent once value is changed
|
||||
* @param {string} props.classNames string caontaining classes to be added in the input field
|
||||
* @param {Array[]} props.modifier_keys
|
||||
*/
|
||||
function HotkeyField({ keys, handleChange, classNames, modifier_keys }) {
|
||||
const inputValue = formatKeysForInput(keys);
|
||||
|
||||
const onInputKeyDown = event => {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
|
||||
hotkeys.record(sequence => {
|
||||
const keys = getKeys({ sequence, modifier_keys });
|
||||
hotkeys.unpause();
|
||||
handleChange(keys);
|
||||
});
|
||||
};
|
||||
|
||||
const onFocus = () => {
|
||||
hotkeys.pause();
|
||||
hotkeys.startRecording();
|
||||
};
|
||||
|
||||
return (
|
||||
<input
|
||||
readOnly={true}
|
||||
type="text"
|
||||
value={inputValue}
|
||||
className={classNames}
|
||||
onKeyDown={onInputKeyDown}
|
||||
onFocus={onFocus}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
HotkeyField.propTypes = {
|
||||
keys: PropTypes.array.isRequired,
|
||||
handleChange: PropTypes.func.isRequired,
|
||||
classNames: PropTypes.string,
|
||||
modifier_keys: PropTypes.array,
|
||||
allowed_keys: PropTypes.array,
|
||||
};
|
||||
|
||||
export { HotkeyField };
|
||||
@@ -0,0 +1 @@
|
||||
export { HotkeyField } from './HotkeyField';
|
||||
@@ -3,11 +3,10 @@ import { LayoutButton, LayoutChooser } from './layoutButton';
|
||||
import { MeasurementTable, MeasurementTableItem } from './measurementTable';
|
||||
import { Overlay, OverlayTrigger } from './overlayTrigger';
|
||||
import { TableList, TableListItem } from './tableList';
|
||||
import {
|
||||
AboutContent,
|
||||
UserPreferences,
|
||||
UserPreferencesForm,
|
||||
} from './userPreferencesForm';
|
||||
import { AboutContent } from './content/aboutContent/AboutContent';
|
||||
import { TabComponents, TabFooter } from './tabComponents';
|
||||
import { HotkeyField } from './customForm';
|
||||
import { LanguageSwitcher } from './languageSwitcher';
|
||||
|
||||
import { Checkbox } from './checkbox';
|
||||
import { CineDialog } from './cineDialog';
|
||||
@@ -48,12 +47,14 @@ export {
|
||||
TableList,
|
||||
TableListItem,
|
||||
Thumbnail,
|
||||
TabComponents,
|
||||
TabFooter,
|
||||
HotkeyField,
|
||||
LanguageSwitcher,
|
||||
TableSearchFilter,
|
||||
TablePagination,
|
||||
ToolbarSection,
|
||||
Tooltip,
|
||||
AboutContent,
|
||||
UserPreferences,
|
||||
UserPreferencesForm,
|
||||
OHIFModal,
|
||||
};
|
||||
@@ -2,26 +2,11 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import './LanguageSwitcher.styl';
|
||||
import { withTranslation } from '../../contextProviders';
|
||||
|
||||
const LanguageSwitcher = ({ language, onLanguageChange }) => {
|
||||
const parseLanguage = lang => lang.split('-')[0];
|
||||
|
||||
const languages = [
|
||||
// TODO: list of available languages should come from i18n.options.resources
|
||||
{
|
||||
value: 'en',
|
||||
label: 'English',
|
||||
},
|
||||
{
|
||||
value: 'es',
|
||||
label: 'Spanish',
|
||||
},
|
||||
];
|
||||
|
||||
const LanguageSwitcher = ({ language, onLanguageChange, languages }) => {
|
||||
const onChange = event => {
|
||||
const { value } = event.target;
|
||||
onLanguageChange(parseLanguage(value));
|
||||
onLanguageChange(value);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -29,7 +14,7 @@ const LanguageSwitcher = ({ language, onLanguageChange }) => {
|
||||
name="language-select"
|
||||
id="language-select"
|
||||
className="language-select"
|
||||
value={parseLanguage(language)}
|
||||
value={language}
|
||||
onChange={onChange}
|
||||
>
|
||||
{languages.map(lng => (
|
||||
@@ -43,7 +28,8 @@ const LanguageSwitcher = ({ language, onLanguageChange }) => {
|
||||
|
||||
LanguageSwitcher.propTypes = {
|
||||
language: PropTypes.string.isRequired,
|
||||
languages: PropTypes.array.isRequired,
|
||||
onLanguageChange: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default withTranslation('UserPreferencesModal')(LanguageSwitcher);
|
||||
export { LanguageSwitcher };
|
||||
@@ -1 +1 @@
|
||||
export { default } from './LanguageSwitcher';
|
||||
export { LanguageSwitcher } from './LanguageSwitcher';
|
||||
@@ -0,0 +1,105 @@
|
||||
import React, { useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
|
||||
// Style
|
||||
import './TabComponents.styl';
|
||||
|
||||
/**
|
||||
* Take name of the tab and create the data-cy value for it
|
||||
*
|
||||
* @param {string} [name=''] tab name
|
||||
* @returns {string} data-cy value
|
||||
*/
|
||||
const getDataCy = (name = '') => {
|
||||
return name
|
||||
.split(' ')
|
||||
.join('-')
|
||||
.toLowerCase();
|
||||
};
|
||||
|
||||
/**
|
||||
* Single tab data information
|
||||
*
|
||||
* @typedef {Object} tabData
|
||||
* @property {string} name - name of the tab
|
||||
* @property {Object} Component - tab component to be rendered
|
||||
* @property {Object} customProps - tab custom properties
|
||||
* @property {bool} hidden - bool to define if tab is hidden of not
|
||||
*/
|
||||
|
||||
/**
|
||||
* Take a list of components data and render then into tabs
|
||||
*
|
||||
* @param {Object} props
|
||||
* @param {[tabData]} props.tabs array of tab data
|
||||
* @param {Object} props.customProps common custom properties
|
||||
*/
|
||||
function TabComponents({ tabs, customProps = {} }) {
|
||||
const [currentTabIndex, setCurrentTabIndex] = useState(0);
|
||||
|
||||
return (
|
||||
tabs.length > 0 && (
|
||||
<div className="TabComponents">
|
||||
<div className="TabComponents_tabHeader">
|
||||
<div className="TabComponents_tabHeader_selector">
|
||||
<div className="dialog-separator-after">
|
||||
<ul className="nav nav-tabs">
|
||||
{tabs.map((tab, index) => {
|
||||
const { name, hidden } = tab;
|
||||
return (
|
||||
!hidden && (
|
||||
<li
|
||||
key={index}
|
||||
onClick={() => {
|
||||
setCurrentTabIndex(index);
|
||||
}}
|
||||
className={classnames(
|
||||
'nav-link',
|
||||
index === currentTabIndex && 'active'
|
||||
)}
|
||||
data-cy={getDataCy(name)}
|
||||
>
|
||||
<button>{name}</button>
|
||||
</li>
|
||||
)
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{tabs.map((tab, index) => {
|
||||
const { Component, customProps: tabCustomProps, hidden } = tab;
|
||||
return (
|
||||
!hidden && (
|
||||
<div
|
||||
key={index}
|
||||
className={classnames(
|
||||
'TabComponents_content',
|
||||
index === currentTabIndex && 'active'
|
||||
)}
|
||||
>
|
||||
<Component {...customProps} {...tabCustomProps} />
|
||||
</div>
|
||||
)
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
TabComponents.propTypes = {
|
||||
tabs: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
name: PropTypes.string,
|
||||
Component: PropTypes.any,
|
||||
customProps: PropTypes.object,
|
||||
hidden: PropTypes.bool,
|
||||
})
|
||||
),
|
||||
customProps: PropTypes.object,
|
||||
};
|
||||
|
||||
export { TabComponents };
|
||||
@@ -0,0 +1,21 @@
|
||||
@import './../../design/styles/common/navbar.styl'
|
||||
|
||||
.TabComponents
|
||||
.TabComponents_tabHeader
|
||||
display: flex
|
||||
flex-direction: column
|
||||
margin-left: -20px
|
||||
margin-right: -20px
|
||||
|
||||
.TabComponents_tabHeader_selector
|
||||
border-bottom: 3px solid black
|
||||
padding: 0 25px
|
||||
|
||||
.TabComponents_content
|
||||
min-height: 450px
|
||||
display: none
|
||||
|
||||
&.active
|
||||
display: flex
|
||||
flex-direction: column
|
||||
justify-content: space-between
|
||||
@@ -0,0 +1,54 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import './TabFooter.styl';
|
||||
|
||||
// In case translate is not passed
|
||||
const translate = word => word;
|
||||
|
||||
function TabFooter({
|
||||
onResetPreferences,
|
||||
onSave,
|
||||
onCancel,
|
||||
hasErrors,
|
||||
t = translate,
|
||||
}) {
|
||||
return (
|
||||
<div className="footer">
|
||||
<button
|
||||
className="btn btn-danger pull-left"
|
||||
data-cy="reset-default-btn"
|
||||
onClick={onResetPreferences}
|
||||
>
|
||||
{t('Reset to Defaults')}
|
||||
</button>
|
||||
<div>
|
||||
<div
|
||||
onClick={onCancel}
|
||||
data-cy="cancel-btn"
|
||||
className="btn btn-default"
|
||||
>
|
||||
{t('Cancel')}
|
||||
</div>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
data-cy="save-btn"
|
||||
disabled={hasErrors}
|
||||
onClick={onSave}
|
||||
>
|
||||
{t('Save')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
TabFooter.propTypes = {
|
||||
onResetPreferences: PropTypes.func,
|
||||
onSave: PropTypes.func,
|
||||
onCancel: PropTypes.func,
|
||||
hasErrors: PropTypes.bool,
|
||||
t: PropTypes.func,
|
||||
};
|
||||
|
||||
export { TabFooter };
|
||||
@@ -0,0 +1,11 @@
|
||||
.footer
|
||||
display: flex
|
||||
flex-direction: row
|
||||
justify-content: space-between
|
||||
padding: 20px 20px 0 20px
|
||||
margin: 0 -20px
|
||||
border-top: 3px solid var(--primary-background-color)
|
||||
|
||||
div
|
||||
button:last-child
|
||||
margin-left: 10px
|
||||
@@ -0,0 +1,27 @@
|
||||
---
|
||||
name: Tab Components
|
||||
menu: Components
|
||||
route: /components/tab-components
|
||||
---
|
||||
|
||||
import { Playground, Props } from 'docz'
|
||||
import { State } from 'react-powerplug'
|
||||
import { TabComponents } from './../index.js'
|
||||
|
||||
// Data
|
||||
import tabs from './tabs.js'
|
||||
|
||||
# Tab Components
|
||||
|
||||
## Basic usage
|
||||
|
||||
<Playground>
|
||||
<TabComponents
|
||||
tabs={tabs}
|
||||
customProps={}
|
||||
/>
|
||||
</Playground>
|
||||
|
||||
## API
|
||||
|
||||
<Props of={TabComponents} />
|
||||
@@ -0,0 +1,26 @@
|
||||
export default tabs = [
|
||||
{
|
||||
name: 'tabName1',
|
||||
Component: () => {
|
||||
return <div>tab 1 Content</div>;
|
||||
},
|
||||
customProps: {},
|
||||
hidden: false,
|
||||
},
|
||||
{
|
||||
name: 'tabName2',
|
||||
Component: () => {
|
||||
return <div>tab 2 Content</div>;
|
||||
},
|
||||
customProps: {},
|
||||
hidden: false,
|
||||
},
|
||||
{
|
||||
name: 'tabName3',
|
||||
Component: () => {
|
||||
return <div>tab 3 Content</div>;
|
||||
},
|
||||
customProps: {},
|
||||
hidden: true,
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,2 @@
|
||||
export { TabComponents } from './TabComponents';
|
||||
export { TabFooter } from './TabFooter';
|
||||
@@ -1,57 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import LanguageSwitcher from '../languageSwitcher';
|
||||
import i18n from '@ohif/i18n';
|
||||
|
||||
/**
|
||||
* General Preferences tab
|
||||
*/
|
||||
|
||||
/**
|
||||
* General Preferences tab
|
||||
* It renders the General Preferences content
|
||||
*
|
||||
* 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.name Tab`s name
|
||||
* @param {object} props.generalPreferences Data for initial state
|
||||
* @param {function} props.onTabStateChanged Callback function to communicate parent in case its states changes
|
||||
* @param {function} props.onTabErrorChanged Callback Function in case any error on tab
|
||||
*/
|
||||
function GeneralPreferences({
|
||||
generalPreferences,
|
||||
name,
|
||||
onTabStateChanged,
|
||||
onTabErrorChanged,
|
||||
}) {
|
||||
const { language = i18n.language } = generalPreferences;
|
||||
|
||||
const onLanguageChange = language => {
|
||||
onTabStateChanged(name, {
|
||||
generalPreferences: { ...generalPreferences, language },
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="general-preferences-wrapper">
|
||||
<div className="col-sm-3">
|
||||
<label htmlFor="language-select" className="p-r-1">
|
||||
Language
|
||||
</label>
|
||||
<LanguageSwitcher
|
||||
language={language}
|
||||
onLanguageChange={onLanguageChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
GeneralPreferences.propTypes = {
|
||||
generalPreferences: PropTypes.any,
|
||||
name: PropTypes.string,
|
||||
onTabStateChanged: PropTypes.func,
|
||||
onTabErrorChanged: PropTypes.func,
|
||||
};
|
||||
|
||||
export { GeneralPreferences };
|
||||
@@ -1,452 +0,0 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import './HotKeysPreferences.styl';
|
||||
|
||||
import {
|
||||
allowedKeys,
|
||||
disallowedCombinations,
|
||||
specialKeys,
|
||||
} from './hotKeysConfig.js';
|
||||
|
||||
import isEqual from 'lodash.isequal';
|
||||
|
||||
const getKeysPressedArray = keyDownEvent => {
|
||||
const keysPressedArray = [];
|
||||
const { ctrlKey, altKey, shiftKey } = keyDownEvent;
|
||||
|
||||
if (ctrlKey && !altKey) {
|
||||
keysPressedArray.push('ctrl');
|
||||
}
|
||||
|
||||
if (shiftKey && !altKey) {
|
||||
keysPressedArray.push('shift');
|
||||
}
|
||||
|
||||
if (altKey && !ctrlKey) {
|
||||
keysPressedArray.push('alt');
|
||||
}
|
||||
|
||||
return keysPressedArray;
|
||||
};
|
||||
|
||||
const findConflictingCommand = (
|
||||
originalHotKeys,
|
||||
currentCommandName,
|
||||
currentHotKeys
|
||||
) => {
|
||||
let firstConflictingCommand = undefined;
|
||||
|
||||
for (const commandName in originalHotKeys) {
|
||||
const toolHotKeys = originalHotKeys[commandName].keys;
|
||||
|
||||
if (
|
||||
isEqual(toolHotKeys, currentHotKeys) &&
|
||||
commandName !== currentCommandName
|
||||
) {
|
||||
firstConflictingCommand = originalHotKeys[commandName];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return firstConflictingCommand;
|
||||
};
|
||||
|
||||
/**
|
||||
* Splits given keysObj into arrays. Each array item will be a representation of column
|
||||
* @param {obj} keysObj objects to be splitted into columns
|
||||
* @param {number} columnSize How many rows per column
|
||||
*/
|
||||
const getHotKeysArrayColumns = (keysObj = {}, columnSize) => {
|
||||
if (isNaN(columnSize)) {
|
||||
return keysObj;
|
||||
}
|
||||
|
||||
const keys = Object.keys(keysObj);
|
||||
const keysValues = Object.values(keysObj);
|
||||
const keysLength = keys.length;
|
||||
|
||||
// Columns from left should be bigger;
|
||||
let currentColumn = 0;
|
||||
const dividedKeys = [];
|
||||
|
||||
for (
|
||||
let it = 0;
|
||||
it < keysLength;
|
||||
it++, it % columnSize === 0 ? currentColumn++ : currentColumn
|
||||
) {
|
||||
if (!dividedKeys[currentColumn]) {
|
||||
dividedKeys[currentColumn] = [];
|
||||
}
|
||||
|
||||
dividedKeys[currentColumn][keys[it]] = keysValues[it];
|
||||
}
|
||||
|
||||
return dividedKeys;
|
||||
};
|
||||
|
||||
const NO_FIELD_ERROR_MESSAGE = undefined;
|
||||
const formatPressedKeys = pressedKeysArray => pressedKeysArray.join('+');
|
||||
const unFormatPressedKeys = (pressedKeysStr = '') => pressedKeysStr.split('+');
|
||||
const inputValidators = (
|
||||
commandName,
|
||||
inputValue,
|
||||
pressedKeys,
|
||||
lastPressedKey,
|
||||
originalHotKeys
|
||||
) => {
|
||||
let hasError = false;
|
||||
let errorMessage = NO_FIELD_ERROR_MESSAGE;
|
||||
|
||||
const modifierValidator = ({ lastPressedKey }) => {
|
||||
// Check if it has a valid modifier
|
||||
const isModifier = ['ctrl', 'alt', 'shift'].includes(lastPressedKey);
|
||||
if (isModifier) {
|
||||
hasError = true;
|
||||
errorMessage =
|
||||
"It's not possible to define only modifier keys (ctrl, alt and shift) as a shortcut";
|
||||
return {
|
||||
hasError,
|
||||
errorMessage,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const emptyValidator = ({ inputValue }) => {
|
||||
if (!inputValue) {
|
||||
hasError = true;
|
||||
errorMessage = "Field can't be empty.";
|
||||
return {
|
||||
hasError,
|
||||
errorMessage,
|
||||
};
|
||||
}
|
||||
};
|
||||
const conflictingValidator = ({
|
||||
commandName,
|
||||
pressedKeys,
|
||||
originalHotKeys,
|
||||
}) => {
|
||||
const conflictingCommand = findConflictingCommand(
|
||||
originalHotKeys,
|
||||
commandName,
|
||||
pressedKeys
|
||||
);
|
||||
if (conflictingCommand) {
|
||||
hasError = true;
|
||||
errorMessage = `"${conflictingCommand.label}" is already using the "${pressedKeys}" shortcut.`;
|
||||
return {
|
||||
hasError,
|
||||
errorMessage,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const disallowedValidator = ({ inputValue, pressedKeys, lastPressedKey }) => {
|
||||
const modifierCommand = formatPressedKeys(
|
||||
pressedKeys.slice(0, pressedKeys.length - 1)
|
||||
);
|
||||
|
||||
const disallowedCombination = disallowedCombinations[modifierCommand];
|
||||
const hasDisallowedCombinations = disallowedCombination
|
||||
? disallowedCombination.includes(lastPressedKey)
|
||||
: false;
|
||||
|
||||
if (hasDisallowedCombinations) {
|
||||
hasError = true;
|
||||
errorMessage = `"${inputValue}" shortcut combination is not allowed`;
|
||||
return {
|
||||
hasError,
|
||||
errorMessage,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const validators = [
|
||||
emptyValidator,
|
||||
modifierValidator,
|
||||
conflictingValidator,
|
||||
disallowedValidator,
|
||||
];
|
||||
|
||||
for (const validator of validators) {
|
||||
const validation = validator({
|
||||
commandName,
|
||||
inputValue,
|
||||
pressedKeys,
|
||||
lastPressedKey,
|
||||
originalHotKeys,
|
||||
});
|
||||
if (validation && validation.hasError) {
|
||||
return validation;
|
||||
}
|
||||
}
|
||||
|
||||
// validation has passed successfully
|
||||
return {
|
||||
hasError,
|
||||
errorMessage,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* HotKeysPreferencesRow
|
||||
* Renders row for hotkey preference
|
||||
* 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.commandName command name associated to given row
|
||||
* @param {string[]} props.hotkeys keys associated to given command
|
||||
* @param {object} props.originalHotKeys original hotkeys values
|
||||
* @param {function} props.onSuccessChanged Callback function to communicate parent in case its states changes
|
||||
* @param {function} props.onFailureChanged Callback Function in case any error on row
|
||||
*/
|
||||
function HotKeyPreferencesRow({
|
||||
commandName,
|
||||
hotkeys,
|
||||
label,
|
||||
originalHotKeys,
|
||||
tabError,
|
||||
onSuccessChanged,
|
||||
onFailureChanged,
|
||||
}) {
|
||||
const [inputValue, setInputValue] = useState(formatPressedKeys(hotkeys));
|
||||
const [error, setError] = useState(false);
|
||||
|
||||
const [fieldErrorMessage, setFieldErrorMessage] = useState(
|
||||
NO_FIELD_ERROR_MESSAGE
|
||||
);
|
||||
|
||||
// reset error count if tab has no errors
|
||||
useEffect(() => {
|
||||
if (!tabError) {
|
||||
setError(false);
|
||||
setFieldErrorMessage(NO_FIELD_ERROR_MESSAGE);
|
||||
setInputValue(formatPressedKeys(hotkeys));
|
||||
}
|
||||
}, [tabError]);
|
||||
|
||||
// update state values if props changes
|
||||
useEffect(() => {
|
||||
setInputValue(formatPressedKeys(hotkeys));
|
||||
}, [hotkeys]);
|
||||
|
||||
const updateInputText = (keyDownEvent, displayPressedKey = false) => {
|
||||
const pressedKeys = getKeysPressedArray(keyDownEvent);
|
||||
|
||||
if (displayPressedKey) {
|
||||
const specialKeyName = specialKeys[keyDownEvent.which];
|
||||
const keyName =
|
||||
specialKeyName ||
|
||||
keyDownEvent.key ||
|
||||
String.fromCharCode(keyDownEvent.keyCode);
|
||||
|
||||
// ensure lowerCase
|
||||
pressedKeys.push(keyName.toLowerCase());
|
||||
}
|
||||
|
||||
setInputValue(formatPressedKeys(pressedKeys));
|
||||
};
|
||||
|
||||
// validate input value
|
||||
const validateInput = () => {
|
||||
const pressedKeys = unFormatPressedKeys(inputValue);
|
||||
const lastPressedKey = pressedKeys[pressedKeys.length - 1];
|
||||
|
||||
const {
|
||||
hasError = false,
|
||||
errorMessage = NO_FIELD_ERROR_MESSAGE,
|
||||
} = inputValidators(
|
||||
commandName,
|
||||
inputValue,
|
||||
pressedKeys,
|
||||
lastPressedKey,
|
||||
originalHotKeys
|
||||
);
|
||||
|
||||
if (hasError) {
|
||||
setInputValue('');
|
||||
} else {
|
||||
onSuccessChanged([inputValue]);
|
||||
}
|
||||
|
||||
if (hasError !== error) {
|
||||
setError(hasError);
|
||||
}
|
||||
|
||||
setFieldErrorMessage(errorMessage);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
onFailureChanged(error);
|
||||
}, [error]);
|
||||
|
||||
const onInputKeyDown = event => {
|
||||
// Prevent ESC key from propagating and closing the modal
|
||||
if (event.key === 'Escape') {
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
updateInputText(event, allowedKeys.includes(event.keyCode));
|
||||
event.preventDefault();
|
||||
};
|
||||
|
||||
return (
|
||||
<tr key={commandName}>
|
||||
<td className="text-right p-r-1">{label}</td>
|
||||
<td width="200">
|
||||
<label
|
||||
className={`wrapperLabel ${
|
||||
fieldErrorMessage !== undefined ? 'state-error' : ''
|
||||
} `}
|
||||
data-key="defaultTool"
|
||||
>
|
||||
<input
|
||||
readOnly={true}
|
||||
type="text"
|
||||
value={inputValue}
|
||||
className="form-control hotkey text-center"
|
||||
onKeyDown={onInputKeyDown}
|
||||
onBlur={validateInput}
|
||||
/>
|
||||
<span className="wrapperText" />
|
||||
<span className="errorMessage">{fieldErrorMessage}</span>
|
||||
</label>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
HotKeyPreferencesRow.propTypes = {
|
||||
commandName: PropTypes.string.isRequired,
|
||||
hotkeys: PropTypes.array.isRequired,
|
||||
label: PropTypes.string.isRequired,
|
||||
originalHotKeys: PropTypes.object.isRequired,
|
||||
tabError: PropTypes.bool.isRequired,
|
||||
onSuccessChanged: PropTypes.func.isRequired,
|
||||
onFailureChanged: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
/**
|
||||
* 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.name Tab`s name
|
||||
* @param {object} props.hotkeyDefinitions Data for initial state
|
||||
* @param {function} props.onTabStateChanged Callback function to communicate parent in case its states changes
|
||||
* @param {function} props.onTabErrorChanged Callback Function in case any error on tab
|
||||
*/
|
||||
function HotKeysPreferences({
|
||||
hotkeyDefinitions,
|
||||
name,
|
||||
tabError,
|
||||
onTabStateChanged,
|
||||
onTabErrorChanged,
|
||||
}) {
|
||||
const [tabState, setTabState] = useState(hotkeyDefinitions);
|
||||
const [tabErrorCounter, setTabErrorCounter] = useState(0);
|
||||
|
||||
const [numColumns] = useState(2);
|
||||
const [columnSize] = useState(() =>
|
||||
Math.ceil(Object.keys(tabState || {}).length / numColumns)
|
||||
);
|
||||
|
||||
const splittedHotKeys = getHotKeysArrayColumns(tabState, columnSize);
|
||||
|
||||
const onHotKeyChanged = (commandName, hotkeyDefinition, keys) => {
|
||||
const newState = {
|
||||
...tabState,
|
||||
[commandName]: { ...hotkeyDefinition, keys },
|
||||
};
|
||||
setTabState(newState);
|
||||
onTabStateChanged(name, { hotkeyDefinitions: newState });
|
||||
};
|
||||
|
||||
const onErrorChanged = (toInc = true) => {
|
||||
const increment = toInc ? 1 : -1;
|
||||
const newValue = tabErrorCounter + increment;
|
||||
if (newValue >= 0) {
|
||||
setTabErrorCounter(newValue);
|
||||
}
|
||||
};
|
||||
|
||||
// reset error count if tab has no errors
|
||||
useEffect(() => {
|
||||
if (!tabError) {
|
||||
setTabErrorCounter(0);
|
||||
// update tab state
|
||||
setTabState({ ...hotkeyDefinitions });
|
||||
}
|
||||
}, [tabError]);
|
||||
|
||||
// tell parent to update its state
|
||||
useEffect(() => {
|
||||
if (tabErrorCounter === 0) {
|
||||
onTabErrorChanged(name, false);
|
||||
}
|
||||
|
||||
if (tabErrorCounter === 1) {
|
||||
onTabErrorChanged(name, true);
|
||||
}
|
||||
}, [tabErrorCounter]);
|
||||
|
||||
// update local state if parent updates
|
||||
useEffect(() => {
|
||||
setTabState({ ...hotkeyDefinitions });
|
||||
}, [hotkeyDefinitions]);
|
||||
|
||||
return (
|
||||
<div className="HotKeysPreferences">
|
||||
{splittedHotKeys.length > 0
|
||||
? splittedHotKeys.map((columnHotKeys, index) => {
|
||||
return (
|
||||
<div className="column" key={index}>
|
||||
<table className="full-width">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="text-right p-r-1">Function</th>
|
||||
<th className="text-center">Shortcut</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{Object.entries(columnHotKeys).map(
|
||||
hotkeyDefinitionTuple => (
|
||||
<HotKeyPreferencesRow
|
||||
key={hotkeyDefinitionTuple[0]}
|
||||
commandName={hotkeyDefinitionTuple[0]}
|
||||
hotkeys={hotkeyDefinitionTuple[1].keys}
|
||||
label={hotkeyDefinitionTuple[1].label}
|
||||
originalHotKeys={tabState}
|
||||
tabError={tabError}
|
||||
onSuccessChanged={keys =>
|
||||
onHotKeyChanged(
|
||||
hotkeyDefinitionTuple[0],
|
||||
hotkeyDefinitionTuple[1],
|
||||
keys
|
||||
)
|
||||
}
|
||||
onFailureChanged={onErrorChanged}
|
||||
></HotKeyPreferencesRow>
|
||||
)
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
: null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
HotKeysPreferences.propTypes = {
|
||||
hotkeyDefinitions: PropTypes.any,
|
||||
name: PropTypes.string,
|
||||
tabError: PropTypes.bool,
|
||||
onTabStateChanged: PropTypes.func,
|
||||
onTabErrorChanged: PropTypes.func,
|
||||
};
|
||||
|
||||
export { HotKeysPreferences };
|
||||
@@ -1,12 +0,0 @@
|
||||
.HotKeysPreferences
|
||||
display: flex;
|
||||
|
||||
.column
|
||||
width: 50%;
|
||||
padding-right: 15px;
|
||||
padding-left: 15px;
|
||||
|
||||
.column-full
|
||||
width: 100%;
|
||||
padding-right: 15px;
|
||||
padding-left: 15px;
|
||||
@@ -1,132 +0,0 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { HotKeysPreferences } from './HotKeysPreferences';
|
||||
import { WindowLevelPreferences } from './WindowLevelPreferences';
|
||||
import { GeneralPreferences } from './GeneralPreferences';
|
||||
import './UserPreferences.styl';
|
||||
|
||||
export class UserPreferences extends Component {
|
||||
static defaultProps = {
|
||||
hotkeyDefinitions: [],
|
||||
windowLevelData: {},
|
||||
generalPreferences: {},
|
||||
};
|
||||
|
||||
// TODO: Make this more generic. Tabs should not be restricted to these entries
|
||||
static propTypes = {
|
||||
hotkeyDefinitions: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
commandName: PropTypes.string,
|
||||
keys: PropTypes.arrayOf(PropTypes.string),
|
||||
label: PropTypes.string,
|
||||
})
|
||||
).isRequired,
|
||||
windowLevelData: PropTypes.object.isRequired,
|
||||
generalPreferences: PropTypes.object.isRequired,
|
||||
updatePropValue: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
state = {
|
||||
tabIndex: 0,
|
||||
};
|
||||
|
||||
tabClick(tabIndex) {
|
||||
this.setState({ tabIndex });
|
||||
}
|
||||
|
||||
renderHotkeysTab() {
|
||||
return (
|
||||
<form className="form-themed themed">
|
||||
<div className="form-content">
|
||||
<HotKeysPreferences
|
||||
hotkeyDefinitions={this.props.hotkeyDefinitions}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
renderWindowLevelTab() {
|
||||
if (this.props.windowLevelData) {
|
||||
return (
|
||||
<form className="form-themed themed">
|
||||
<div className="form-content">
|
||||
<WindowLevelPreferences
|
||||
windowLevelData={this.props.windowLevelData}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
renderGeneralTab() {
|
||||
return (
|
||||
<form className="form-themed themed">
|
||||
<div className="form-content">
|
||||
<GeneralPreferences
|
||||
generalPreferences={this.props.generalPreferences}
|
||||
updatePropValue={this.props.updatePropValue}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
renderTabs(tabIndex) {
|
||||
switch (tabIndex) {
|
||||
case 0:
|
||||
return this.renderHotkeysTab();
|
||||
/* case 1:
|
||||
return this.renderWindowLevelTab(); */
|
||||
case 2:
|
||||
return this.renderGeneralTab();
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
getTabClass(tabIndex) {
|
||||
return tabIndex === this.state.tabIndex ? 'nav-link active' : 'nav-link';
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="UserPreferences">
|
||||
<div className="UserPreferences__selector">
|
||||
<ul className="nav nav-tabs">
|
||||
<li
|
||||
onClick={() => {
|
||||
this.tabClick(0);
|
||||
}}
|
||||
className={this.getTabClass(0)}
|
||||
>
|
||||
<button>Hotkeys</button>
|
||||
</li>
|
||||
{false && (
|
||||
<li
|
||||
onClick={() => {
|
||||
this.tabClick(1);
|
||||
}}
|
||||
className={this.getTabClass(1)}
|
||||
>
|
||||
<button>Window Level</button>
|
||||
</li>
|
||||
)}
|
||||
<li
|
||||
onClick={() => {
|
||||
this.tabClick(2);
|
||||
}}
|
||||
className={this.getTabClass(2)}
|
||||
>
|
||||
<button>General</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
{this.renderTabs(this.state.tabIndex)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,219 +0,0 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useSnackbarContext } from '@ohif/ui';
|
||||
|
||||
import './UserPreferencesForm.styl';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
// Tabs Component wrapper
|
||||
import { UserPreferencesTabs } from './UserPreferencesTabs';
|
||||
|
||||
// Tabs
|
||||
import { HotKeysPreferences } from './HotKeysPreferences';
|
||||
import { WindowLevelPreferences } from './WindowLevelPreferences';
|
||||
import { GeneralPreferences } from './GeneralPreferences';
|
||||
|
||||
/**
|
||||
@typedef TabObject
|
||||
@type {Object}
|
||||
@property {string} name Name for given tab
|
||||
@property {ReactComponent} Component React component for given tab.
|
||||
@property {object} props Props State for given tab component
|
||||
@property {boolean} [hidden] To hidden tab or not
|
||||
*/
|
||||
|
||||
/**
|
||||
* Create tabs obj.
|
||||
* @returns {TabObject[]} Array of TabObjs.
|
||||
*/
|
||||
const createTabs = () => {
|
||||
return [
|
||||
{
|
||||
name: 'Hotkeys',
|
||||
Component: HotKeysPreferences,
|
||||
props: {},
|
||||
},
|
||||
{
|
||||
name: 'General',
|
||||
Component: GeneralPreferences,
|
||||
props: {},
|
||||
},
|
||||
{
|
||||
name: 'Window Level',
|
||||
Component: WindowLevelPreferences,
|
||||
props: {},
|
||||
hidden: true,
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
/**
|
||||
* Main form component to render preferences tabs and buttons
|
||||
* @param {object} props component props
|
||||
* @param {string} props.name Tab`s name
|
||||
* @param {object} props.hotkeyDefinitions Hotkeys Data
|
||||
* @param {object} props.windowLevelData Window level data
|
||||
* @param {function} props.onSave Callback function when saving
|
||||
* @param {function} props.onClose Callback function when closing
|
||||
* @param {function} props.onResetToDefaults Callback function when resetting
|
||||
*/
|
||||
function UserPreferencesForm({
|
||||
onClose,
|
||||
onSave,
|
||||
onResetToDefaults,
|
||||
windowLevelData,
|
||||
hotkeyDefinitions,
|
||||
generalPreferences,
|
||||
hotkeysManager,
|
||||
defaultLanguage,
|
||||
hotkeyDefaults,
|
||||
}) {
|
||||
const [tabs, setTabs] = useState(createTabs());
|
||||
|
||||
const createTabsState = (
|
||||
windowLevelData,
|
||||
hotkeyDefinitions,
|
||||
generalPreferences
|
||||
) => {
|
||||
return {
|
||||
Hotkeys: { hotkeyDefinitions },
|
||||
'Window Level': { windowLevelData },
|
||||
General: { generalPreferences },
|
||||
};
|
||||
};
|
||||
|
||||
const [tabsState, setTabsState] = useState(
|
||||
createTabsState(windowLevelData, hotkeyDefinitions, generalPreferences)
|
||||
);
|
||||
|
||||
const [tabsError, setTabsError] = useState(
|
||||
tabs.reduce((acc, tab) => {
|
||||
acc[tab.name] = false;
|
||||
return acc;
|
||||
}, {})
|
||||
);
|
||||
|
||||
const snackbar = useSnackbarContext();
|
||||
|
||||
const { t, ready: translationsAreReady } = useTranslation(
|
||||
'UserPreferencesModal'
|
||||
);
|
||||
|
||||
const onTabStateChanged = (tabName, newState) => {
|
||||
setTabsState({ ...tabsState, [tabName]: newState });
|
||||
};
|
||||
|
||||
const onTabErrorChanged = (tabName, hasError) => {
|
||||
setTabsError({ ...tabsError, [tabName]: hasError });
|
||||
};
|
||||
|
||||
const hasAnyError = () => {
|
||||
return Object.values(tabsError).reduce((acc, value) => acc || value);
|
||||
};
|
||||
|
||||
const onResetPreferences = () => {
|
||||
const defaultHotKeyDefitions = {};
|
||||
|
||||
hotkeyDefaults.map(item => {
|
||||
const { commandName, ...values } = item;
|
||||
defaultHotKeyDefitions[commandName] = { ...values };
|
||||
});
|
||||
|
||||
// update local state
|
||||
setTabsState({
|
||||
...tabsState,
|
||||
Hotkeys: { hotkeyDefinitions: defaultHotKeyDefitions },
|
||||
General: { generalPreferences: { language: defaultLanguage } },
|
||||
});
|
||||
|
||||
// update tabs state
|
||||
setTabs(createTabs(windowLevelData, hotkeyDefinitions, generalPreferences));
|
||||
|
||||
// reset errors
|
||||
setTabsError(
|
||||
tabs.reduce((acc, tab) => {
|
||||
acc[tab.name] = false;
|
||||
return acc;
|
||||
}, {})
|
||||
);
|
||||
|
||||
snackbar.show({
|
||||
message: (
|
||||
<div dangerouslySetInnerHTML={{ __html: t('ResetDefaultMessage') }} />
|
||||
),
|
||||
type: 'info',
|
||||
});
|
||||
};
|
||||
|
||||
const onSavePreferences = event => {
|
||||
const toSave = Object.values(tabsState).reduce((acc, tabState) => {
|
||||
return { ...acc, ...tabState };
|
||||
}, {});
|
||||
|
||||
onSave(toSave);
|
||||
snackbar.show({
|
||||
message: t('SaveMessage'),
|
||||
type: 'success',
|
||||
});
|
||||
};
|
||||
|
||||
// update local state if prop values changes
|
||||
useEffect(() => {
|
||||
setTabsState(
|
||||
createTabsState(windowLevelData, hotkeyDefinitions, generalPreferences)
|
||||
);
|
||||
}, [windowLevelData, hotkeyDefinitions, generalPreferences]);
|
||||
|
||||
return translationsAreReady ? (
|
||||
<div className="UserPreferencesForm">
|
||||
<UserPreferencesTabs
|
||||
tabs={tabs}
|
||||
tabsState={tabsState}
|
||||
tabsError={tabsError}
|
||||
onTabStateChanged={onTabStateChanged}
|
||||
onTabErrorChanged={onTabErrorChanged}
|
||||
/>
|
||||
<div className="footer">
|
||||
<button
|
||||
className="btn btn-danger pull-left"
|
||||
data-cy="reset-default-btn"
|
||||
onClick={onResetPreferences}
|
||||
>
|
||||
{t('Reset to Defaults')}
|
||||
</button>
|
||||
<div>
|
||||
<div
|
||||
onClick={onClose}
|
||||
data-cy="cancel-btn"
|
||||
className="btn btn-default"
|
||||
>
|
||||
{t('Cancel')}
|
||||
</div>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
data-cy="save-btn"
|
||||
disabled={hasAnyError()}
|
||||
onClick={onSavePreferences}
|
||||
>
|
||||
{t('Save')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null;
|
||||
}
|
||||
|
||||
UserPreferencesForm.propTypes = {
|
||||
onClose: PropTypes.func,
|
||||
onSave: PropTypes.func,
|
||||
onResetToDefaults: PropTypes.func,
|
||||
windowLevelData: PropTypes.object,
|
||||
hotkeyDefinitions: PropTypes.object,
|
||||
generalPreferences: PropTypes.object,
|
||||
hotkeysManager: PropTypes.object,
|
||||
defaultLanguage: PropTypes.string,
|
||||
hotkeyDefaults: PropTypes.array,
|
||||
};
|
||||
|
||||
export { UserPreferencesForm };
|
||||
@@ -1,23 +0,0 @@
|
||||
@import './../../design/styles/common/navbar.styl'
|
||||
@import './../../design/styles/common/global.styl'
|
||||
@import './../../design/styles/common/modal.styl'
|
||||
@import './../../design/styles/common/button.styl'
|
||||
|
||||
.close
|
||||
float: right;
|
||||
font-size: 21px;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
color: #000;
|
||||
text-shadow: 0 1px 0 #fff;
|
||||
opacity: .2;
|
||||
|
||||
.UserPreferencesForm
|
||||
.footer
|
||||
display: flex
|
||||
flex-direction: row
|
||||
justify-content: space-between
|
||||
|
||||
div
|
||||
button:last-child
|
||||
margin-left: 10px
|
||||
@@ -1,107 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import './UserPreferencesTabs.styl';
|
||||
|
||||
/**
|
||||
* Render tab component
|
||||
* @param {object} tab TabObject containing tab data
|
||||
* @param {function} onTabStateChanged Callback Function in case tab changes its state
|
||||
* @param {function} onTabErrorChanged Callback Function in case any error on tab
|
||||
*/
|
||||
const renderTab = (
|
||||
tab = {},
|
||||
tabsState,
|
||||
tabsError,
|
||||
onTabStateChanged,
|
||||
onTabErrorChanged
|
||||
) => {
|
||||
const { props, Component, name, hidden = false } = tab;
|
||||
|
||||
const tabState = tabsState[name];
|
||||
const tabError = tabsError[name];
|
||||
|
||||
return !hidden ? (
|
||||
<form className="form-themed themed tabs">
|
||||
<div className="form-content">
|
||||
<Component
|
||||
key={name}
|
||||
{...tabState}
|
||||
{...props}
|
||||
name={name}
|
||||
tabError={tabError}
|
||||
onTabStateChanged={onTabStateChanged}
|
||||
onTabErrorChanged={onTabErrorChanged}
|
||||
></Component>
|
||||
</div>
|
||||
</form>
|
||||
) : null;
|
||||
};
|
||||
|
||||
const renderTabsHeader = (tabs, activeTabIndex, onHeaderChanged) => {
|
||||
return tabs.length > 0
|
||||
? tabs.map((tab, index) => {
|
||||
const { name, hidden = false } = tab;
|
||||
|
||||
const cypressSelectorId = name.toLowerCase();
|
||||
const tabClass =
|
||||
index === activeTabIndex ? 'nav-link active' : 'nav-link';
|
||||
return !hidden ? (
|
||||
<li
|
||||
key={name}
|
||||
onClick={() => {
|
||||
onHeaderChanged(index);
|
||||
}}
|
||||
className={tabClass}
|
||||
data-cy={cypressSelectorId}
|
||||
>
|
||||
<button>{name}</button>
|
||||
</li>
|
||||
) : null;
|
||||
})
|
||||
: null;
|
||||
};
|
||||
/**
|
||||
* Component to render tabs based on currentActiveTabIndex
|
||||
*
|
||||
* In case any tab changes its state this current component tells parent through function callback
|
||||
* @param {object} props Component props
|
||||
*/
|
||||
function UserPreferencesTabs({
|
||||
tabs,
|
||||
tabsState,
|
||||
tabsError,
|
||||
onTabStateChanged,
|
||||
onTabErrorChanged,
|
||||
}) {
|
||||
const [activeTabIndex, setActiveTabIndex] = useState(0);
|
||||
|
||||
return (
|
||||
<div className="UserPreferencesTabs">
|
||||
<div className="UserPreferencesTabs__selector">
|
||||
<div className="dialog-separator-after">
|
||||
<ul className="nav nav-tabs">
|
||||
{renderTabsHeader(tabs, activeTabIndex, setActiveTabIndex)}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{renderTab(
|
||||
tabs[activeTabIndex],
|
||||
tabsState,
|
||||
tabsError,
|
||||
onTabStateChanged,
|
||||
onTabErrorChanged
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
UserPreferencesTabs.propTypes = {
|
||||
tabs: PropTypes.array.isRequired,
|
||||
tabsState: PropTypes.object.isRequired,
|
||||
tabsError: PropTypes.object.isRequired,
|
||||
onTabStateChanged: PropTypes.func.isRequired,
|
||||
onTabErrorChanged: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export { UserPreferencesTabs };
|
||||
@@ -1,29 +0,0 @@
|
||||
@import './../../design/styles/common/form.styl'
|
||||
@import './../../design/styles/common/navbar.styl'
|
||||
@import './../../design/styles/common/state.styl'
|
||||
@import './../../design/styles/common/global.styl'
|
||||
|
||||
.UserPreferencesTabs
|
||||
display: flex
|
||||
flex-direction: column
|
||||
|
||||
&__selector
|
||||
border-bottom: 3px solid black
|
||||
|
||||
.errorMessage
|
||||
color: var(--state-error-text)
|
||||
font-size: 10px
|
||||
text-transform: uppercase;
|
||||
|
||||
.form-content
|
||||
border-bottom: 3px solid var(--primary-background-color)
|
||||
margin-bottom: 20px
|
||||
margin-left: -20px
|
||||
margin-right: -20px
|
||||
max-height: 70vh
|
||||
overflow-y: auto
|
||||
padding: 20px
|
||||
min-height: 500px
|
||||
|
||||
.popover
|
||||
width: 300px
|
||||
@@ -1,132 +0,0 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import './WindowLevelPreferences.styl';
|
||||
/**
|
||||
* WindowLevelPreferencesRow
|
||||
* Renders row for window level preference
|
||||
* 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.description description for given preset
|
||||
* @param {number} props.window window value
|
||||
* @param {number} props.level level value
|
||||
* @param {string} props.rowName name of given row to identify it
|
||||
* @param {function} props.onSuccessChanged Callback function to communicate parent in case its states changes
|
||||
*/
|
||||
function WindowLevelPreferencesRow({
|
||||
description,
|
||||
window,
|
||||
level,
|
||||
rowName,
|
||||
onSuccessChanged,
|
||||
// onFailureChanged
|
||||
}) {
|
||||
const [rowState, setRowState] = useState({ description, window, level });
|
||||
|
||||
const onInputChanged = (event, name) => {
|
||||
const newValue = event.target.value;
|
||||
setRowState({ ...rowState, [name]: newValue });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
onSuccessChanged(rowName, rowState);
|
||||
}, [rowState]);
|
||||
|
||||
const renderTd = (value, name, type) => {
|
||||
return (
|
||||
<td className="p-r-1">
|
||||
<label className="wrapperLabel">
|
||||
<input
|
||||
value={value}
|
||||
type={type}
|
||||
className="form-control"
|
||||
onChange={event => {
|
||||
onInputChanged(event, name);
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
</td>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<tr key={rowName}>
|
||||
<td className="p-r-1 text-center">{rowName}</td>
|
||||
{renderTd(rowState.description, 'description', 'text')}
|
||||
{renderTd(rowState.window, 'window', 'number')}
|
||||
{renderTd(rowState.level, 'level', 'number')}
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
WindowLevelPreferencesRow.propTypes = {
|
||||
description: PropTypes.string.isRequired,
|
||||
window: PropTypes.number.isRequired,
|
||||
level: PropTypes.number.isRequired,
|
||||
rowName: PropTypes.string.isRequired,
|
||||
onSuccessChanged: PropTypes.func.isRequired,
|
||||
//onFailureChanged: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
/**
|
||||
* WindowLevelPreferences tab
|
||||
* It renders all window level presets
|
||||
*
|
||||
* 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.name Tab`s name
|
||||
* @param {object} props.windowLevelData Data for initial state
|
||||
* @param {function} props.onTabStateChanged Callback function to communicate parent in case its states changes
|
||||
*/
|
||||
function WindowLevelPreferences({
|
||||
windowLevelData,
|
||||
name,
|
||||
onTabStateChanged /*onTabErrorChanged*/,
|
||||
}) {
|
||||
const [tabState, setTabState] = useState(windowLevelData);
|
||||
// TODO to be used once error handling is implemented
|
||||
//const [tabError, setTabError] = useState(false);
|
||||
|
||||
const onWindowLevelChanged = (key, state) => {
|
||||
setTabState({ ...tabState, [key]: state });
|
||||
};
|
||||
|
||||
// tell parent to update its state
|
||||
useEffect(() => {
|
||||
onTabStateChanged(name, { windowLevelData: tabState });
|
||||
}, [tabState]);
|
||||
|
||||
return (
|
||||
<table className="full-width">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="p-x-1 text-center presetIndex">Preset</th>
|
||||
<th className="p-x-1">Description</th>
|
||||
<th className="p-x-1">Window</th>
|
||||
<th className="p-x-1">Level</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{Object.keys(tabState).map(objKey => (
|
||||
<WindowLevelPreferencesRow
|
||||
onSuccessChanged={onWindowLevelChanged}
|
||||
rowName={objKey}
|
||||
key={objKey}
|
||||
description={tabState[objKey].description}
|
||||
window={tabState[objKey].window}
|
||||
level={tabState[objKey].level}
|
||||
></WindowLevelPreferencesRow>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
|
||||
WindowLevelPreferences.propTypes = {
|
||||
windowLevelData: PropTypes.object.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
onTabStateChanged: PropTypes.func.isRequired,
|
||||
//onTabErrorChanged: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export { WindowLevelPreferences };
|
||||
@@ -1,4 +0,0 @@
|
||||
@import './UserPreferencesTabs.styl'
|
||||
|
||||
.presetIndex
|
||||
padding: 0px 10px 0px 10px
|
||||
@@ -1,14 +0,0 @@
|
||||
export default {
|
||||
currentLanguage: 'en',
|
||||
languages: [
|
||||
{
|
||||
value: 'en',
|
||||
label: 'English',
|
||||
},
|
||||
{
|
||||
value: 'es',
|
||||
label: 'Spanish',
|
||||
},
|
||||
],
|
||||
onChange: language => {},
|
||||
};
|
||||
@@ -1,75 +0,0 @@
|
||||
export default {
|
||||
defaultTool: { label: 'Default Tool', keys: ['ESC'], column: 0 },
|
||||
zoom: { label: 'Zoom', keys: ['Z'], column: 0 },
|
||||
wwwc: { label: 'W/L', keys: ['W'], column: 0 },
|
||||
pan: { label: 'Pan', keys: ['P'], column: 0 },
|
||||
angle: { label: 'Angle measurement', keys: ['A'], column: 0 },
|
||||
stackScroll: { label: 'Scroll stack', keys: ['S'], column: 0 },
|
||||
magnify: { label: 'Magnify', keys: ['M'], column: 0 },
|
||||
length: { label: 'Length measurement', keys: [''], column: 0 },
|
||||
annotate: { label: 'Annotate', keys: [''], column: 0 },
|
||||
dragProbe: { label: 'Pixel probe', keys: [''], column: 0 },
|
||||
ellipticalRoi: { label: 'Elliptical ROI', keys: [''], column: 0 },
|
||||
rectangleRoi: { label: 'Rectangle ROI', keys: [''], column: 0 },
|
||||
|
||||
// Viewport hotkeys
|
||||
flipH: { label: 'Flip Horizontally', keys: ['H'], column: 0 },
|
||||
flipV: { label: 'Flip Vertically', keys: ['V'], column: 0 },
|
||||
rotateR: { label: 'Rotate Right', keys: ['R'], column: 0 },
|
||||
rotateL: { label: 'Rotate Left', keys: ['L'], column: 0 },
|
||||
invert: { label: 'Invert', keys: ['I'], column: 0 },
|
||||
zoomIn: { label: 'Zoom In', keys: [''], column: 0 },
|
||||
zoomOut: { label: 'Zoom Out', keys: [''], column: 0 },
|
||||
zoomToFit: { label: 'Zoom to Fit', keys: [''], column: 0 },
|
||||
resetViewport: { label: 'Reset', keys: [''], column: 0 },
|
||||
clearTools: { label: 'Clear Tools', keys: [''], column: 0 },
|
||||
|
||||
// 2nd column
|
||||
|
||||
// Viewport navigation hotkeys
|
||||
scrollDown: { label: 'Scroll Down', keys: ['DOWN'], column: 1 },
|
||||
scrollUp: { label: 'Scroll Up', keys: ['UP'], column: 1 },
|
||||
scrollLastImage: { label: 'Scroll to Last Image', keys: ['END'], column: 1 },
|
||||
scrollFirstImage: {
|
||||
label: 'Scroll to First Image',
|
||||
keys: ['HOME'],
|
||||
column: 1,
|
||||
},
|
||||
previousDisplaySet: {
|
||||
label: 'Previous Series',
|
||||
keys: ['PAGEUP'],
|
||||
column: 1,
|
||||
},
|
||||
nextDisplaySet: { label: 'Next Series', keys: ['PAGEDOWN'], column: 1 },
|
||||
nextPanel: { label: 'Next Image Viewport', keys: ['RIGHT'], column: 1 },
|
||||
previousPanel: {
|
||||
label: 'Previous Image Viewport',
|
||||
keys: ['LEFT'],
|
||||
column: 1,
|
||||
},
|
||||
|
||||
// Miscellaneous hotkeys
|
||||
toggleOverlayTags: {
|
||||
label: 'Toggle Image Info Overlay',
|
||||
keys: ['O'],
|
||||
column: 1,
|
||||
},
|
||||
toggleCinePlay: { label: 'Play/Pause Cine', keys: ['SPACE'], column: 1 },
|
||||
toggleCineDialog: {
|
||||
label: 'Show/Hide Cine Controls',
|
||||
keys: [''],
|
||||
column: 1,
|
||||
},
|
||||
|
||||
// Preset hotkeys
|
||||
WLPreset0: { label: 'W/L Preset 0 (Soft Tissue)', keys: ['1'], column: 1 },
|
||||
WLPreset1: { label: 'W/L Preset 1 (Lung)', keys: ['2'], column: 1 },
|
||||
WLPreset2: { label: 'W/L Preset 2 (Liver)', keys: ['3'], column: 1 },
|
||||
WLPreset3: { label: 'W/L Preset 3 (Bone)', keys: ['4'], column: 1 },
|
||||
WLPreset4: { label: 'W/L Preset 4 (Brain)', keys: ['5'], column: 1 },
|
||||
WLPreset5: { label: 'W/L Preset 5', keys: ['6'], column: 1 },
|
||||
WLPreset6: { label: 'W/L Preset 6', keys: ['7'], column: 1 },
|
||||
WLPreset7: { label: 'W/L Preset 7', keys: ['8'], column: 1 },
|
||||
WLPreset8: { label: 'W/L Preset 8', keys: ['9'], column: 1 },
|
||||
WLPreset9: { label: 'W/L Preset 0', keys: ['0'], column: 1 },
|
||||
};
|
||||
@@ -1,54 +0,0 @@
|
||||
---
|
||||
name: User Preferences Form
|
||||
menu: Components
|
||||
route: /components/user-preferences-form
|
||||
---
|
||||
|
||||
import { Playground, Props } from 'docz'
|
||||
import { State } from 'react-powerplug'
|
||||
import { UserPreferencesForm } from './../index.js'
|
||||
import NameSpace from '../../../__docs__/NameSpace'
|
||||
//
|
||||
import windowLevelDefaults from './windowLevelDefaults.js'
|
||||
import hotkeyDefaults from './hotkeyDefaults.js'
|
||||
|
||||
# User Preferences Form
|
||||
|
||||
## Basic usage
|
||||
|
||||
<Playground>
|
||||
<State initial={{
|
||||
isOpen: false,
|
||||
windowLevelData: windowLevelDefaults,
|
||||
hotkeyDefinitions: hotkeyDefaults,
|
||||
}}>
|
||||
|
||||
{({ state, setState }) => (
|
||||
<React.Fragment>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
type="button"
|
||||
onClick={ () => setState({ isOpen: true }) }
|
||||
>
|
||||
Open user preferences
|
||||
</button>
|
||||
<UserPreferencesForm
|
||||
{...state}
|
||||
onCancel={() => setState({ isOpen: false })}
|
||||
onSave={() => alert('on save')}
|
||||
onResetToDefaults={() => alert('on reset')}
|
||||
/>
|
||||
</React.Fragment>
|
||||
)}
|
||||
|
||||
</State>
|
||||
|
||||
</Playground>
|
||||
|
||||
## API
|
||||
|
||||
<Props of={UserPreferencesForm} />
|
||||
|
||||
## Translation Namespace
|
||||
|
||||
<NameSpace name="UserPreferencesForm" />
|
||||
@@ -1,13 +0,0 @@
|
||||
export default {
|
||||
0: { description: 'Soft tissue', window: 400, level: 40 },
|
||||
1: { description: 'Lung', window: 1500, level: -600 },
|
||||
2: { description: 'Liver', window: 150, level: 90 },
|
||||
3: { description: 'Bone', window: 2500, level: 480 },
|
||||
4: { description: 'Brain', window: 80, level: 40 },
|
||||
5: { description: '', window: '', level: '' },
|
||||
6: { description: '', window: '', level: '' },
|
||||
7: { description: '', window: '', level: '' },
|
||||
8: { description: '', window: '', level: '' },
|
||||
9: { description: '', window: '', level: '' },
|
||||
10: { description: '', window: '', level: '' },
|
||||
};
|
||||
@@ -1,100 +0,0 @@
|
||||
const range = (start, end) => {
|
||||
return new Array(end - start).fill().map((d, i) => i + start);
|
||||
};
|
||||
|
||||
export const disallowedCombinations = {
|
||||
'': [],
|
||||
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 allowedKeys = [
|
||||
...[8, 13, 27, 32, 46], // backspace, enter, escape, space, delete
|
||||
...[12, 106, 107, 109, 110, 111], // Numpad keys
|
||||
...range(218, 220), // [\]
|
||||
...range(185, 190), // ;=,-./
|
||||
...range(111, 131), // F1-F19
|
||||
...range(32, 41), // arrow keys, home/end, pg dn/up
|
||||
...range(47, 58), // 0-9
|
||||
...range(64, 91), // A-Z
|
||||
];
|
||||
|
||||
export const specialKeys = {
|
||||
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,4 +0,0 @@
|
||||
export { UserPreferencesTabs } from './UserPreferencesTabs.js';
|
||||
export { AboutContent } from '../content/aboutContent/AboutContent.js';
|
||||
export { UserPreferencesForm } from './UserPreferencesForm.js';
|
||||
export { GeneralPreferences } from './GeneralPreferences.js';
|
||||
@@ -19,13 +19,15 @@ import {
|
||||
TableList,
|
||||
TableListItem,
|
||||
Thumbnail,
|
||||
TabComponents,
|
||||
TabFooter,
|
||||
HotkeyField,
|
||||
LanguageSwitcher,
|
||||
TableSearchFilter,
|
||||
TablePagination,
|
||||
ToolbarSection,
|
||||
Tooltip,
|
||||
AboutContent,
|
||||
UserPreferences,
|
||||
UserPreferencesForm,
|
||||
OHIFModal,
|
||||
} from './components';
|
||||
import { useDebounce, useMedia } from './hooks';
|
||||
@@ -98,6 +100,10 @@ export {
|
||||
TableList,
|
||||
TableListItem,
|
||||
Thumbnail,
|
||||
TabComponents,
|
||||
TabFooter,
|
||||
HotkeyField,
|
||||
LanguageSwitcher,
|
||||
TableSearchFilter,
|
||||
TablePagination,
|
||||
Toolbar,
|
||||
@@ -105,8 +111,6 @@ export {
|
||||
ToolbarSection,
|
||||
Tooltip,
|
||||
AboutContent,
|
||||
UserPreferences,
|
||||
UserPreferencesForm,
|
||||
ViewerbaseDragDropContext,
|
||||
SnackbarProvider,
|
||||
useSnackbarContext,
|
||||
|
||||
Reference in new issue
Block a user