diff --git a/platform/core/src/__mocks__/log.js b/platform/core/src/__mocks__/log.js
index 29194b790..474a5887f 100644
--- a/platform/core/src/__mocks__/log.js
+++ b/platform/core/src/__mocks__/log.js
@@ -1,4 +1,5 @@
export default {
warn: jest.fn(),
error: jest.fn(),
+ info: jest.fn(),
};
diff --git a/platform/core/src/classes/HotkeysManager.js b/platform/core/src/classes/HotkeysManager.js
index 781f79251..ee65f08a3 100644
--- a/platform/core/src/classes/HotkeysManager.js
+++ b/platform/core/src/classes/HotkeysManager.js
@@ -1,4 +1,4 @@
-import hotkeys from './hotkeys';
+import hotkeys from './../utils/hotkeys';
import log from './../log.js';
/**
@@ -11,7 +11,7 @@ import log from './../log.js';
*/
export class HotkeysManager {
- constructor(commandsManager) {
+ constructor(commandsManager, servicesManager) {
this.hotkeyDefinitions = {};
this.hotkeyDefaults = [];
this.isEnabled = true;
@@ -22,9 +22,19 @@ export class HotkeysManager {
);
}
+ this._servicesManager = servicesManager;
this._commandsManager = commandsManager;
}
+ /**
+ * Exposes Mousetrap.js's `.record` method, added by the record plugin.
+ *
+ * @param {*} event
+ */
+ record(event) {
+ return hotkeys.record(event);
+ }
+
/**
* Disables all hotkeys. Hotkeys added while disabled will not listen for
* input.
@@ -45,38 +55,57 @@ export class HotkeysManager {
/**
* Registers a list of hotkeydefinitions.
*
- * @param {HotkeyDefinition[] | Object} hotkeyDefinitions Contains hotkeys definitions
+ * @param {HotkeyDefinition[] | Object} [hotkeyDefinitions=[]] Contains hotkeys definitions
*/
- setHotkeys(hotkeyDefinitions) {
- const definitions = Array.isArray(hotkeyDefinitions)
- ? [...hotkeyDefinitions]
- : this._parseToArrayLike(hotkeyDefinitions);
+ setHotkeys(hotkeyDefinitions = []) {
+ try {
+ const definitions = this._getValidDefinitions(hotkeyDefinitions);
- definitions.forEach(definition => this.registerHotkeys(definition));
+ definitions.forEach(definition => this.registerHotkeys(definition));
+ } catch (error) {
+ const { UINotificationService } = this._servicesManager.services;
+ UINotificationService.show({
+ title: 'Hotkeys Manager',
+ message: 'Erro while setting hotkeys',
+ type: 'error',
+ });
+ }
}
/**
* Set default hotkey bindings. These
* values are used in `this.restoreDefaultBindings`.
*
- * @param {HotkeyDefinition[] | Object} hotkeyDefinitions Contains hotkeys definitions
+ * @param {HotkeyDefinition[] | Object} [hotkeyDefinitions=[]] Contains hotkeys definitions
*/
- setDefaultHotKeys(hotkeyDefinitions) {
+ setDefaultHotKeys(hotkeyDefinitions = []) {
+ const definitions = this._getValidDefinitions(hotkeyDefinitions);
+
+ this.hotkeyDefaults = definitions;
+ }
+
+ /**
+ * Take hotkey definitions that can be an array or object and make sure that it
+ * returns an array of hotkeys
+ *
+ * @param {HotkeyDefinition[] | Object} [hotkeyDefinitions=[]] Contains hotkeys definitions
+ */
+ _getValidDefinitions(hotkeyDefinitions) {
const definitions = Array.isArray(hotkeyDefinitions)
? [...hotkeyDefinitions]
: this._parseToArrayLike(hotkeyDefinitions);
- this.hotkeyDefaults = definitions;
+ return definitions;
}
/**
* It parses given object containing hotkeyDefinition to array like.
* Each property of given object will be mapped to an object of an array. And its property name will be the value of a property named as commandName
*
- * @param {HotkeyDefinition[] | Object} hotkeyDefinitions Contains hotkeys definitions
+ * @param {HotkeyDefinition[] | Object} [hotkeyDefinitions={}] Contains hotkeys definitions
* @returns {HotkeyDefinition[]}
*/
- _parseToArrayLike(hotkeyDefinitionsObj) {
+ _parseToArrayLike(hotkeyDefinitionsObj = {}) {
const copy = { ...hotkeyDefinitionsObj };
return Object.entries(copy).map(entryValue =>
this._parseToHotKeyObj(entryValue[0], entryValue[1])
@@ -127,11 +156,13 @@ export class HotkeysManager {
if (previouslyRegisteredDefinition) {
const previouslyRegisteredKeys = previouslyRegisteredDefinition.keys;
this._unbindHotkeys(commandName, previouslyRegisteredKeys);
+ log.info(`Unbinding ${commandName} from ${previouslyRegisteredKeys}`);
}
// Set definition & bind
this.hotkeyDefinitions[commandName] = { keys, label };
this._bindHotkeys(commandName, keys);
+ log.info(`Binding ${commandName} to ${keys}`);
}
/**
@@ -167,12 +198,11 @@ export class HotkeysManager {
}
const isKeyArray = keys instanceof Array;
- if (isKeyArray) {
- keys.forEach(key => this._bindHotkeys(commandName, key));
- return;
- }
+ const combinedKeys = isKeyArray ? keys.join('+') : keys;
- hotkeys.bind(keys, evt => {
+ hotkeys.bind(combinedKeys, evt => {
+ evt.preventDefault();
+ evt.stopPropagation();
this._commandsManager.runCommand(commandName, { evt });
});
}
@@ -193,7 +223,8 @@ export class HotkeysManager {
const isKeyArray = keys instanceof Array;
if (isKeyArray) {
- keys.forEach(key => this._unbindHotkeys(commandName, key));
+ const combinedKeys = keys.join('+');
+ this._unbindHotkeys(commandName, combinedKeys);
return;
}
diff --git a/platform/core/src/classes/HotkeysManager.test.js b/platform/core/src/classes/HotkeysManager.test.js
index c980c5b1a..68bedceee 100644
--- a/platform/core/src/classes/HotkeysManager.test.js
+++ b/platform/core/src/classes/HotkeysManager.test.js
@@ -1,10 +1,10 @@
import CommandsManager from './CommandsManager.js';
import HotkeysManager from './HotkeysManager.js';
-import hotkeys from './hotkeys';
+import hotkeys from './../utils/hotkeys';
import log from './../log.js';
jest.mock('./CommandsManager.js');
-jest.mock('./hotkeys');
+jest.mock('./../utils/hotkeys');
jest.mock('./../log.js');
describe('HotkeysManager', () => {
@@ -60,7 +60,10 @@ describe('HotkeysManager', () => {
});
describe('enable()', () => {
- beforeEach(() => hotkeys.unpause.mockClear());
+ beforeEach(() => {
+ hotkeys.unpause = jest.fn();
+ hotkeys.unpause.mockClear();
+ });
it('sets isEnabled property to true', () => {
hotkeysManager.disable();
@@ -139,20 +142,18 @@ describe('HotkeysManager', () => {
expectedHotkeyDefinition
);
});
- it('calls hotkeys.bind for all keys in array', () => {
- const definition = { commandName: 'dance', keys: ['h', 'e', 'l', 'o'] };
+ it('calls hotkeys.bind for the group of keys', () => {
+ const definition = { commandName: 'dance', keys: ['shift', 'e'] };
hotkeysManager.registerHotkeys(definition);
- expect(hotkeys.bind.mock.calls.length).toBe(definition.keys.length);
- definition.keys.forEach((key, i) =>
- expect(hotkeys.bind.mock.calls[i][0]).toBe(key)
- );
+ expect(hotkeys.bind.mock.calls.length).toBe(1);
+ expect(hotkeys.bind.mock.calls[0][0]).toBe('shift+e');
});
it('calls hotkeys.unbind if commandName was previously registered, for each previously registered set of keys', () => {
const firstDefinition = {
commandName: 'dance',
- keys: ['h', 'e', 'l', 'o'],
+ keys: ['alt', 'e'],
};
const secondDefinition = { commandName: 'dance', keys: 'a' };
@@ -161,12 +162,8 @@ describe('HotkeysManager', () => {
// Second call
hotkeysManager.registerHotkeys(secondDefinition);
- expect(hotkeys.unbind.mock.calls.length).toBe(
- firstDefinition.keys.length
- );
- firstDefinition.keys.forEach((key, i) =>
- expect(hotkeys.unbind.mock.calls[i][0]).toBe(key)
- );
+ expect(hotkeys.unbind.mock.calls.length).toBe(1);
+ expect(hotkeys.unbind.mock.calls[0][0]).toBe('alt+e');
});
});
diff --git a/platform/core/src/classes/hotkeys/index.js b/platform/core/src/classes/hotkeys/index.js
deleted file mode 100644
index 4db77861a..000000000
--- a/platform/core/src/classes/hotkeys/index.js
+++ /dev/null
@@ -1,17 +0,0 @@
-// Only imported in environment w/ `window`
-// So we need to mock these for tests
-import Mousetrap from 'mousetrap';
-import pausePlugin from 'mousetrap/plugins/pause/mousetrap-pause.js';
-import recordPlugin from 'mousetrap/plugins/record/mousetrap-record.js';
-
-// import pausePlugin from './pausePlugin.js';
-// import recordPlugin from './recordPlugin.js';
-
-// // // TODO: May need to bind these so Mousetrap = this in plugins;
-// pausePlugin(Mousetrap);
-// recordPlugin(Mousetrap);
-
-// console.log(Mousetrap);
-// console.log(Object.keys(Mousetrap));
-
-export default Mousetrap;
diff --git a/platform/core/src/index.js b/platform/core/src/index.js
index f0b8dd2d4..d8b3586b3 100644
--- a/platform/core/src/index.js
+++ b/platform/core/src/index.js
@@ -18,7 +18,7 @@ import string from './string.js';
import studies from './studies/';
import ui from './ui';
import user from './user.js';
-import utils from './utils/';
+import utils, { hotkeys } from './utils/';
import {
UINotificationService,
@@ -36,6 +36,7 @@ const OHIF = {
ServicesManager,
//
utils,
+ hotkeys,
studies,
redux,
classes,
@@ -68,6 +69,7 @@ export {
ServicesManager,
//
utils,
+ hotkeys,
studies,
redux,
classes,
diff --git a/platform/core/src/index.test.js b/platform/core/src/index.test.js
index ac35a2543..cc0e2a690 100644
--- a/platform/core/src/index.test.js
+++ b/platform/core/src/index.test.js
@@ -16,6 +16,7 @@ describe('Top level exports', () => {
'MeasurementService',
//
'utils',
+ 'hotkeys',
'studies',
'redux',
'classes',
diff --git a/platform/core/src/measurements/tools/angle.js b/platform/core/src/measurements/tools/angle.js
index 852cc37e5..4fc1a05f4 100644
--- a/platform/core/src/measurements/tools/angle.js
+++ b/platform/core/src/measurements/tools/angle.js
@@ -1,6 +1,6 @@
const displayFunction = data => {
let text = '';
- if (data.rAngle) {
+ if (data.rAngle && !isNaN(data.rAngle)) {
text = data.rAngle.toFixed(2) + String.fromCharCode(parseInt('00B0', 16));
}
return text;
diff --git a/platform/core/src/measurements/tools/circleRoi.js b/platform/core/src/measurements/tools/circleRoi.js
index 0f8cef3fe..6be0f396a 100644
--- a/platform/core/src/measurements/tools/circleRoi.js
+++ b/platform/core/src/measurements/tools/circleRoi.js
@@ -1,7 +1,7 @@
const displayFunction = data => {
let meanValue = '';
const { cachedStats } = data;
- if (cachedStats && cachedStats.mean) {
+ if (cachedStats && cachedStats.mean && !isNaN(cachedStats.mean)) {
meanValue = cachedStats.mean.toFixed(2) + ' HU';
}
return meanValue;
diff --git a/platform/core/src/measurements/tools/ellipticalRoi.js b/platform/core/src/measurements/tools/ellipticalRoi.js
index f392002d2..ba6a070f4 100644
--- a/platform/core/src/measurements/tools/ellipticalRoi.js
+++ b/platform/core/src/measurements/tools/ellipticalRoi.js
@@ -1,7 +1,7 @@
const displayFunction = data => {
let meanValue = '';
const { cachedStats } = data;
- if (cachedStats && cachedStats.mean) {
+ if (cachedStats && cachedStats.mean && !isNaN(cachedStats.mean)) {
meanValue = cachedStats.mean.toFixed(2) + ' HU';
}
return meanValue;
diff --git a/platform/core/src/measurements/tools/freehandMouse.js b/platform/core/src/measurements/tools/freehandMouse.js
index ea26bd6b2..83ad3ee2d 100644
--- a/platform/core/src/measurements/tools/freehandMouse.js
+++ b/platform/core/src/measurements/tools/freehandMouse.js
@@ -1,6 +1,6 @@
const displayFunction = data => {
let meanValue = '';
- if (data.meanStdDev && data.meanStdDev.mean) {
+ if (data.meanStdDev && data.meanStdDev.mean && !isNaN(data.meanStdDev.mean)) {
meanValue = data.meanStdDev.mean.toFixed(2) + ' HU';
}
return meanValue;
diff --git a/platform/core/src/measurements/tools/length.js b/platform/core/src/measurements/tools/length.js
index 2f8331f5e..ccad2fa01 100644
--- a/platform/core/src/measurements/tools/length.js
+++ b/platform/core/src/measurements/tools/length.js
@@ -1,6 +1,6 @@
const displayFunction = data => {
let lengthValue = '';
- if (data.length) {
+ if (data.length && !isNaN(data.length)) {
lengthValue = data.length.toFixed(2) + ' mm';
}
return lengthValue;
diff --git a/platform/core/src/measurements/tools/rectangleRoi.js b/platform/core/src/measurements/tools/rectangleRoi.js
index 6a3593809..5ac159b6b 100644
--- a/platform/core/src/measurements/tools/rectangleRoi.js
+++ b/platform/core/src/measurements/tools/rectangleRoi.js
@@ -1,7 +1,7 @@
const displayFunction = data => {
let meanValue = '';
const { cachedStats } = data;
- if (cachedStats && cachedStats.mean) {
+ if (cachedStats && cachedStats.mean && !isNaN(cachedStats.mean)) {
meanValue = cachedStats.mean.toFixed(2) + ' HU';
}
return meanValue;
diff --git a/platform/core/src/redux/reducers/preferences.js b/platform/core/src/redux/reducers/preferences.js
index c04ea2603..0af7754ab 100644
--- a/platform/core/src/redux/reducers/preferences.js
+++ b/platform/core/src/redux/reducers/preferences.js
@@ -1,12 +1,6 @@
import cloneDeep from 'lodash.clonedeep';
const defaultState = {
- // First tab
- hotkeyDefinitions: [
- // commandName, label, keys
- // [{ zoom: { label: 'Zoom', keys: ['z'] }}]
- ],
- // Second tab
windowLevelData: {
// order, description, window (int), level (int)
// 0: { description: 'Soft tissue', window: '', level: '' },
diff --git a/platform/core/src/services/UINotificationService/index.js b/platform/core/src/services/UINotificationService/index.js
index 7b3e28c12..0cdb8ea95 100644
--- a/platform/core/src/services/UINotificationService/index.js
+++ b/platform/core/src/services/UINotificationService/index.js
@@ -12,6 +12,8 @@
const name = 'UINotificationService';
+const serviceShowRequestQueue = [];
+
const publicAPI = {
name,
hide: _hide,
@@ -21,7 +23,11 @@ const publicAPI = {
const serviceImplementation = {
_hide: () => console.warn('hide() NOT IMPLEMENTED'),
- _show: () => console.warn('show() NOT IMPLEMENTED'),
+ _show: showArguments => {
+ serviceShowRequestQueue.push(showArguments);
+
+ console.warn('show() NOT IMPLEMENTED');
+ },
};
/**
@@ -76,6 +82,11 @@ function setServiceImplementation({
}
if (showImplementation) {
serviceImplementation._show = showImplementation;
+
+ while (serviceShowRequestQueue.length > 0) {
+ const showArguments = serviceShowRequestQueue.pop();
+ serviceImplementation._show(showArguments);
+ }
}
}
diff --git a/platform/core/src/utils/hotkeys/index.js b/platform/core/src/utils/hotkeys/index.js
new file mode 100644
index 000000000..9cd157a1a
--- /dev/null
+++ b/platform/core/src/utils/hotkeys/index.js
@@ -0,0 +1,8 @@
+import Mousetrap from 'mousetrap';
+import pausePlugin from './pausePlugin';
+import recordPlugin from './recordPlugin';
+
+recordPlugin(Mousetrap);
+pausePlugin(Mousetrap);
+
+export default Mousetrap;
diff --git a/platform/core/src/classes/hotkeys/pausePlugin.js b/platform/core/src/utils/hotkeys/pausePlugin.js
similarity index 100%
rename from platform/core/src/classes/hotkeys/pausePlugin.js
rename to platform/core/src/utils/hotkeys/pausePlugin.js
diff --git a/platform/core/src/classes/hotkeys/recordPlugin.js b/platform/core/src/utils/hotkeys/recordPlugin.js
similarity index 89%
rename from platform/core/src/classes/hotkeys/recordPlugin.js
rename to platform/core/src/utils/hotkeys/recordPlugin.js
index e091039bd..9e4a36df5 100644
--- a/platform/core/src/classes/hotkeys/recordPlugin.js
+++ b/platform/core/src/utils/hotkeys/recordPlugin.js
@@ -66,7 +66,7 @@ export default function(Mousetrap) {
_recordCurrentCombo();
}
- for (i = 0; i < modifiers.length; ++i) {
+ for (let i = 0; i < modifiers.length; ++i) {
_recordKey(modifiers[i]);
}
_recordKey(character);
@@ -85,10 +85,8 @@ export default function(Mousetrap) {
* @returns void
*/
function _recordKey(key) {
- var i;
-
// one-off implementation of Array.indexOf, since IE6-9 don't support it
- for (i = 0; i < _currentRecordedKeys.length; ++i) {
+ for (let i = 0; i < _currentRecordedKeys.length; ++i) {
if (_currentRecordedKeys[i] === key) {
return;
}
@@ -111,7 +109,7 @@ export default function(Mousetrap) {
_recordedSequence.push(_currentRecordedKeys);
_currentRecordedKeys = [];
_recordedCharacterKey = false;
- _restartRecordTimer();
+ _finishRecording();
}
/**
@@ -124,9 +122,7 @@ export default function(Mousetrap) {
* @returns void
*/
function _normalizeSequence(sequence) {
- var i;
-
- for (i = 0; i < sequence.length; ++i) {
+ for (let i = 0; i < sequence.length; ++i) {
sequence[i].sort(function(x, y) {
// modifier keys always come first, in alphabetical order
if (x.length > 1 && y.length === 1) {
@@ -191,6 +187,28 @@ export default function(Mousetrap) {
};
};
+ /**
+ * stop recording
+ *
+ * @param {Function} callback
+ * @returns void
+ */
+ Mousetrap.prototype.stopRecord = function() {
+ var self = this;
+ self.recording = false;
+ };
+
+ /**
+ * start recording
+ *
+ * @param {Function} callback
+ * @returns void
+ */
+ Mousetrap.prototype.startRecording = function() {
+ var self = this;
+ self.recording = true;
+ };
+
Mousetrap.prototype.handleKey = function() {
var self = this;
_handleKey.apply(self, arguments);
diff --git a/platform/core/src/utils/index.js b/platform/core/src/utils/index.js
index 0450358ab..3f9cdcddb 100644
--- a/platform/core/src/utils/index.js
+++ b/platform/core/src/utils/index.js
@@ -12,6 +12,7 @@ import DicomLoaderService from './dicomLoaderService.js';
import b64toBlob from './b64toBlob.js';
import * as urlUtil from './urlUtil';
import makeCancelable from './makeCancelable';
+import hotkeys from './hotkeys';
const utils = {
guid,
@@ -29,6 +30,7 @@ const utils = {
DicomLoaderService,
urlUtil,
makeCancelable,
+ hotkeys,
};
export {
@@ -47,6 +49,7 @@ export {
DicomLoaderService,
urlUtil,
makeCancelable,
+ hotkeys,
};
export default utils;
diff --git a/platform/core/src/utils/index.test.js b/platform/core/src/utils/index.test.js
index 7742dcebd..36909e965 100644
--- a/platform/core/src/utils/index.test.js
+++ b/platform/core/src/utils/index.test.js
@@ -18,6 +18,7 @@ describe('Top level exports', () => {
'DicomLoaderService',
'urlUtil',
'makeCancelable',
+ 'hotkeys',
].sort();
const exports = Object.keys(utils.default).sort();
diff --git a/platform/i18n/src/getAvailableLanguagesInfo.js b/platform/i18n/src/getAvailableLanguagesInfo.js
new file mode 100644
index 000000000..8dd1f257a
--- /dev/null
+++ b/platform/i18n/src/getAvailableLanguagesInfo.js
@@ -0,0 +1,70 @@
+const languagesMap = {
+ ar: 'Arabic',
+ am: 'Amharic',
+ bg: 'Bulgarian',
+ bn: 'Bengali',
+ ca: 'Catalan',
+ cs: 'Czech',
+ da: 'Danish',
+ de: 'German',
+ el: 'Greek',
+ en: 'English',
+ 'en-GB': 'English (Great Britain)',
+ 'en-US': 'English (USA)',
+ es: 'Spanish',
+ et: 'Estonian',
+ fa: 'Persian',
+ fi: 'Finnish',
+ fil: 'Filipino',
+ fr: 'French',
+ gu: 'Gujarati',
+ he: 'Hebrew',
+ hi: 'Hindi',
+ hr: 'Croatian',
+ hu: 'Hungarian',
+ id: 'Indonesian',
+ it: 'Italian',
+ ja: 'Japanese',
+ 'ja-JP': 'Japanese (Japan)',
+ kn: 'Kannada',
+ ko: 'Korean',
+ lt: 'Lithuanian',
+ lv: 'Latvian',
+ ml: 'Malayalam',
+ mr: 'Marathi',
+ ms: 'Malay',
+ nl: 'Dutch',
+ no: 'Norwegian',
+ pl: 'Polish',
+ 'pt-BR': 'Portuguese (Brazil)',
+ 'pt-PT': 'Portuguese (Portugal)',
+ ro: 'Romanian',
+ ru: 'Russian',
+ sk: 'Slovak',
+ sl: 'Slovenian',
+ sr: 'Serbian',
+ sv: 'Swedish',
+ sw: 'Swahili',
+ ta: 'Tamil',
+ te: 'Telugu',
+ th: 'Thai',
+ tr: 'Turkish',
+ uk: 'Ukrainian',
+ vi: 'Vietnamese',
+ zh: 'Chinese',
+ 'zh-CN': 'Chinese (China)',
+ 'zh-TW': 'Chinese (Taiwan)',
+};
+
+export default function getAvailableLanguagesInfo(locales) {
+ const availableLanguagesInfo = [];
+
+ Object.keys(locales).forEach(key => {
+ availableLanguagesInfo.push({
+ value: key,
+ label: languagesMap[key] || key,
+ });
+ });
+
+ return availableLanguagesInfo;
+}
diff --git a/platform/i18n/src/index.js b/platform/i18n/src/index.js
index 1ad95d090..59f2ae76e 100644
--- a/platform/i18n/src/index.js
+++ b/platform/i18n/src/index.js
@@ -140,4 +140,7 @@ i18n.initI18n = initI18n;
i18n.addLocales = addLocales;
i18n.defaultLanguage = DEFAULT_LANGUAGE;
+import getAvailableLanguagesInfo from './getAvailableLanguagesInfo.js';
+i18n.availableLanguages = getAvailableLanguagesInfo(locales);
+
export default i18n;
diff --git a/platform/ui/src/components/userPreferencesForm/__docs__/about.mdx b/platform/ui/src/components/content/aboutContent/__docs__/about.mdx
similarity index 100%
rename from platform/ui/src/components/userPreferencesForm/__docs__/about.mdx
rename to platform/ui/src/components/content/aboutContent/__docs__/about.mdx
diff --git a/platform/ui/src/components/customForm/HotkeyField.js b/platform/ui/src/components/customForm/HotkeyField.js
new file mode 100644
index 000000000..bfc638d9d
--- /dev/null
+++ b/platform/ui/src/components/customForm/HotkeyField.js
@@ -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 (
+
+ );
+}
+
+HotkeyField.propTypes = {
+ keys: PropTypes.array.isRequired,
+ handleChange: PropTypes.func.isRequired,
+ classNames: PropTypes.string,
+ modifier_keys: PropTypes.array,
+ allowed_keys: PropTypes.array,
+};
+
+export { HotkeyField };
diff --git a/platform/ui/src/components/customForm/index.js b/platform/ui/src/components/customForm/index.js
new file mode 100644
index 000000000..7fc6b3435
--- /dev/null
+++ b/platform/ui/src/components/customForm/index.js
@@ -0,0 +1 @@
+export { HotkeyField } from './HotkeyField';
diff --git a/platform/ui/src/components/index.js b/platform/ui/src/components/index.js
index 5daca57b1..3624e4115 100644
--- a/platform/ui/src/components/index.js
+++ b/platform/ui/src/components/index.js
@@ -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,
};
diff --git a/platform/ui/src/components/languageSwitcher/LanguageSwitcher.js b/platform/ui/src/components/languageSwitcher/LanguageSwitcher.js
index da22eb80b..bbd42c977 100644
--- a/platform/ui/src/components/languageSwitcher/LanguageSwitcher.js
+++ b/platform/ui/src/components/languageSwitcher/LanguageSwitcher.js
@@ -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 };
diff --git a/platform/ui/src/components/languageSwitcher/index.js b/platform/ui/src/components/languageSwitcher/index.js
index 31505c03b..d21e84e1e 100644
--- a/platform/ui/src/components/languageSwitcher/index.js
+++ b/platform/ui/src/components/languageSwitcher/index.js
@@ -1 +1 @@
-export { default } from './LanguageSwitcher';
+export { LanguageSwitcher } from './LanguageSwitcher';
diff --git a/platform/ui/src/components/tabComponents/TabComponents.js b/platform/ui/src/components/tabComponents/TabComponents.js
new file mode 100644
index 000000000..c706bf499
--- /dev/null
+++ b/platform/ui/src/components/tabComponents/TabComponents.js
@@ -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 && (
+
+
+
+
+
+ {tabs.map((tab, index) => {
+ const { name, hidden } = tab;
+ return (
+ !hidden && (
+ - {
+ setCurrentTabIndex(index);
+ }}
+ className={classnames(
+ 'nav-link',
+ index === currentTabIndex && 'active'
+ )}
+ data-cy={getDataCy(name)}
+ >
+
+
+ )
+ );
+ })}
+
+
+
+
+ {tabs.map((tab, index) => {
+ const { Component, customProps: tabCustomProps, hidden } = tab;
+ return (
+ !hidden && (
+
+
+
+ )
+ );
+ })}
+
+ )
+ );
+}
+
+TabComponents.propTypes = {
+ tabs: PropTypes.arrayOf(
+ PropTypes.shape({
+ name: PropTypes.string,
+ Component: PropTypes.any,
+ customProps: PropTypes.object,
+ hidden: PropTypes.bool,
+ })
+ ),
+ customProps: PropTypes.object,
+};
+
+export { TabComponents };
diff --git a/platform/ui/src/components/tabComponents/TabComponents.styl b/platform/ui/src/components/tabComponents/TabComponents.styl
new file mode 100644
index 000000000..263b39ae9
--- /dev/null
+++ b/platform/ui/src/components/tabComponents/TabComponents.styl
@@ -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
diff --git a/platform/ui/src/components/tabComponents/TabFooter.js b/platform/ui/src/components/tabComponents/TabFooter.js
new file mode 100644
index 000000000..8247fcc98
--- /dev/null
+++ b/platform/ui/src/components/tabComponents/TabFooter.js
@@ -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 (
+
+
+
+
+ {t('Cancel')}
+
+
+
+
+ );
+}
+
+TabFooter.propTypes = {
+ onResetPreferences: PropTypes.func,
+ onSave: PropTypes.func,
+ onCancel: PropTypes.func,
+ hasErrors: PropTypes.bool,
+ t: PropTypes.func,
+};
+
+export { TabFooter };
diff --git a/platform/ui/src/components/tabComponents/TabFooter.styl b/platform/ui/src/components/tabComponents/TabFooter.styl
new file mode 100644
index 000000000..c44957d93
--- /dev/null
+++ b/platform/ui/src/components/tabComponents/TabFooter.styl
@@ -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
diff --git a/platform/ui/src/components/tabComponents/__docs__/tabComponents.mdx b/platform/ui/src/components/tabComponents/__docs__/tabComponents.mdx
new file mode 100644
index 000000000..5f5e6531f
--- /dev/null
+++ b/platform/ui/src/components/tabComponents/__docs__/tabComponents.mdx
@@ -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
+
+
+
+
+
+## API
+
+
diff --git a/platform/ui/src/components/tabComponents/__docs__/tabs.js b/platform/ui/src/components/tabComponents/__docs__/tabs.js
new file mode 100644
index 000000000..8b2369558
--- /dev/null
+++ b/platform/ui/src/components/tabComponents/__docs__/tabs.js
@@ -0,0 +1,26 @@
+export default tabs = [
+ {
+ name: 'tabName1',
+ Component: () => {
+ return tab 1 Content
;
+ },
+ customProps: {},
+ hidden: false,
+ },
+ {
+ name: 'tabName2',
+ Component: () => {
+ return tab 2 Content
;
+ },
+ customProps: {},
+ hidden: false,
+ },
+ {
+ name: 'tabName3',
+ Component: () => {
+ return tab 3 Content
;
+ },
+ customProps: {},
+ hidden: true,
+ },
+];
diff --git a/platform/ui/src/components/tabComponents/index.js b/platform/ui/src/components/tabComponents/index.js
new file mode 100644
index 000000000..01becf767
--- /dev/null
+++ b/platform/ui/src/components/tabComponents/index.js
@@ -0,0 +1,2 @@
+export { TabComponents } from './TabComponents';
+export { TabFooter } from './TabFooter';
diff --git a/platform/ui/src/components/userPreferencesForm/GeneralPreferences.js b/platform/ui/src/components/userPreferencesForm/GeneralPreferences.js
deleted file mode 100644
index 42ad1e7ec..000000000
--- a/platform/ui/src/components/userPreferencesForm/GeneralPreferences.js
+++ /dev/null
@@ -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 (
-
- );
-}
-
-GeneralPreferences.propTypes = {
- generalPreferences: PropTypes.any,
- name: PropTypes.string,
- onTabStateChanged: PropTypes.func,
- onTabErrorChanged: PropTypes.func,
-};
-
-export { GeneralPreferences };
diff --git a/platform/ui/src/components/userPreferencesForm/HotKeysPreferences.js b/platform/ui/src/components/userPreferencesForm/HotKeysPreferences.js
deleted file mode 100644
index 8458102b5..000000000
--- a/platform/ui/src/components/userPreferencesForm/HotKeysPreferences.js
+++ /dev/null
@@ -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 (
-
- | {label} |
-
-
- |
-
- );
-}
-
-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 (
-
- {splittedHotKeys.length > 0
- ? splittedHotKeys.map((columnHotKeys, index) => {
- return (
-
-
-
-
- | Function |
- Shortcut |
-
-
-
- {Object.entries(columnHotKeys).map(
- hotkeyDefinitionTuple => (
-
- onHotKeyChanged(
- hotkeyDefinitionTuple[0],
- hotkeyDefinitionTuple[1],
- keys
- )
- }
- onFailureChanged={onErrorChanged}
- >
- )
- )}
-
-
-
- );
- })
- : null}
-
- );
-}
-
-HotKeysPreferences.propTypes = {
- hotkeyDefinitions: PropTypes.any,
- name: PropTypes.string,
- tabError: PropTypes.bool,
- onTabStateChanged: PropTypes.func,
- onTabErrorChanged: PropTypes.func,
-};
-
-export { HotKeysPreferences };
diff --git a/platform/ui/src/components/userPreferencesForm/HotKeysPreferences.styl b/platform/ui/src/components/userPreferencesForm/HotKeysPreferences.styl
deleted file mode 100644
index d34cbbbaa..000000000
--- a/platform/ui/src/components/userPreferencesForm/HotKeysPreferences.styl
+++ /dev/null
@@ -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;
diff --git a/platform/ui/src/components/userPreferencesForm/UserPreferences.js b/platform/ui/src/components/userPreferencesForm/UserPreferences.js
deleted file mode 100644
index 9ee6b07b2..000000000
--- a/platform/ui/src/components/userPreferencesForm/UserPreferences.js
+++ /dev/null
@@ -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 (
-
- );
- }
-
- renderWindowLevelTab() {
- if (this.props.windowLevelData) {
- return (
-
- );
- }
- }
-
- renderGeneralTab() {
- return (
-
- );
- }
-
- 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 (
-
-
-
- - {
- this.tabClick(0);
- }}
- className={this.getTabClass(0)}
- >
-
-
- {false && (
- - {
- this.tabClick(1);
- }}
- className={this.getTabClass(1)}
- >
-
-
- )}
- - {
- this.tabClick(2);
- }}
- className={this.getTabClass(2)}
- >
-
-
-
-
- {this.renderTabs(this.state.tabIndex)}
-
- );
- }
-}
diff --git a/platform/ui/src/components/userPreferencesForm/UserPreferencesForm.js b/platform/ui/src/components/userPreferencesForm/UserPreferencesForm.js
deleted file mode 100644
index 893ecd318..000000000
--- a/platform/ui/src/components/userPreferencesForm/UserPreferencesForm.js
+++ /dev/null
@@ -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: (
-
- ),
- 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 ? (
-
-
-
-
-
-
- {t('Cancel')}
-
-
-
-
-
- ) : 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 };
diff --git a/platform/ui/src/components/userPreferencesForm/UserPreferencesForm.styl b/platform/ui/src/components/userPreferencesForm/UserPreferencesForm.styl
deleted file mode 100644
index 4d6c60d42..000000000
--- a/platform/ui/src/components/userPreferencesForm/UserPreferencesForm.styl
+++ /dev/null
@@ -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
diff --git a/platform/ui/src/components/userPreferencesForm/UserPreferencesTabs.js b/platform/ui/src/components/userPreferencesForm/UserPreferencesTabs.js
deleted file mode 100644
index 47fd764c6..000000000
--- a/platform/ui/src/components/userPreferencesForm/UserPreferencesTabs.js
+++ /dev/null
@@ -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 ? (
-
- ) : 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 ? (
- {
- onHeaderChanged(index);
- }}
- className={tabClass}
- data-cy={cypressSelectorId}
- >
-
-
- ) : 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 (
-
-
-
-
- {renderTabsHeader(tabs, activeTabIndex, setActiveTabIndex)}
-
-
-
- {renderTab(
- tabs[activeTabIndex],
- tabsState,
- tabsError,
- onTabStateChanged,
- onTabErrorChanged
- )}
-
- );
-}
-
-UserPreferencesTabs.propTypes = {
- tabs: PropTypes.array.isRequired,
- tabsState: PropTypes.object.isRequired,
- tabsError: PropTypes.object.isRequired,
- onTabStateChanged: PropTypes.func.isRequired,
- onTabErrorChanged: PropTypes.func.isRequired,
-};
-
-export { UserPreferencesTabs };
diff --git a/platform/ui/src/components/userPreferencesForm/UserPreferencesTabs.styl b/platform/ui/src/components/userPreferencesForm/UserPreferencesTabs.styl
deleted file mode 100644
index e7197167a..000000000
--- a/platform/ui/src/components/userPreferencesForm/UserPreferencesTabs.styl
+++ /dev/null
@@ -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
diff --git a/platform/ui/src/components/userPreferencesForm/WindowLevelPreferences.js b/platform/ui/src/components/userPreferencesForm/WindowLevelPreferences.js
deleted file mode 100644
index 381050caa..000000000
--- a/platform/ui/src/components/userPreferencesForm/WindowLevelPreferences.js
+++ /dev/null
@@ -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 (
-
-
- |
- );
- };
-
- return (
-
- | {rowName} |
- {renderTd(rowState.description, 'description', 'text')}
- {renderTd(rowState.window, 'window', 'number')}
- {renderTd(rowState.level, 'level', 'number')}
-
- );
-}
-
-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 (
-
-
-
- | Preset |
- Description |
- Window |
- Level |
-
-
-
- {Object.keys(tabState).map(objKey => (
-
- ))}
-
-
- );
-}
-
-WindowLevelPreferences.propTypes = {
- windowLevelData: PropTypes.object.isRequired,
- name: PropTypes.string.isRequired,
- onTabStateChanged: PropTypes.func.isRequired,
- //onTabErrorChanged: PropTypes.func.isRequired,
-};
-
-export { WindowLevelPreferences };
diff --git a/platform/ui/src/components/userPreferencesForm/WindowLevelPreferences.styl b/platform/ui/src/components/userPreferencesForm/WindowLevelPreferences.styl
deleted file mode 100644
index fd49a7936..000000000
--- a/platform/ui/src/components/userPreferencesForm/WindowLevelPreferences.styl
+++ /dev/null
@@ -1,4 +0,0 @@
-@import './UserPreferencesTabs.styl'
-
-.presetIndex
- padding: 0px 10px 0px 10px
diff --git a/platform/ui/src/components/userPreferencesForm/__docs__/generalDefaults.js b/platform/ui/src/components/userPreferencesForm/__docs__/generalDefaults.js
deleted file mode 100644
index 40f7e3387..000000000
--- a/platform/ui/src/components/userPreferencesForm/__docs__/generalDefaults.js
+++ /dev/null
@@ -1,14 +0,0 @@
-export default {
- currentLanguage: 'en',
- languages: [
- {
- value: 'en',
- label: 'English',
- },
- {
- value: 'es',
- label: 'Spanish',
- },
- ],
- onChange: language => {},
-};
diff --git a/platform/ui/src/components/userPreferencesForm/__docs__/hotkeyDefaults.js b/platform/ui/src/components/userPreferencesForm/__docs__/hotkeyDefaults.js
deleted file mode 100644
index 64f3f3035..000000000
--- a/platform/ui/src/components/userPreferencesForm/__docs__/hotkeyDefaults.js
+++ /dev/null
@@ -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 },
-};
diff --git a/platform/ui/src/components/userPreferencesForm/__docs__/userPreferences.mdx b/platform/ui/src/components/userPreferencesForm/__docs__/userPreferences.mdx
deleted file mode 100644
index 8bff79a90..000000000
--- a/platform/ui/src/components/userPreferencesForm/__docs__/userPreferences.mdx
+++ /dev/null
@@ -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
-
-
-
-
- {({ state, setState }) => (
-
-
- setState({ isOpen: false })}
- onSave={() => alert('on save')}
- onResetToDefaults={() => alert('on reset')}
- />
-
- )}
-
-
-
-
-
-## API
-
-
-
-## Translation Namespace
-
-
diff --git a/platform/ui/src/components/userPreferencesForm/__docs__/windowLevelDefaults.js b/platform/ui/src/components/userPreferencesForm/__docs__/windowLevelDefaults.js
deleted file mode 100644
index ba1990c84..000000000
--- a/platform/ui/src/components/userPreferencesForm/__docs__/windowLevelDefaults.js
+++ /dev/null
@@ -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: '' },
-};
diff --git a/platform/ui/src/components/userPreferencesForm/index.js b/platform/ui/src/components/userPreferencesForm/index.js
deleted file mode 100644
index 612cc490c..000000000
--- a/platform/ui/src/components/userPreferencesForm/index.js
+++ /dev/null
@@ -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';
diff --git a/platform/ui/src/index.js b/platform/ui/src/index.js
index 88e3aee77..451ac244e 100644
--- a/platform/ui/src/index.js
+++ b/platform/ui/src/index.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,
diff --git a/platform/viewer/cypress/integration/common/OHIFUserPreferences.spec.js b/platform/viewer/cypress/integration/common/OHIFUserPreferences.spec.js
index a77216e1e..e6eace64b 100644
--- a/platform/viewer/cypress/integration/common/OHIFUserPreferences.spec.js
+++ b/platform/viewer/cypress/integration/common/OHIFUserPreferences.spec.js
@@ -30,9 +30,8 @@ describe('OHIF User Preferences', () => {
});
it('checks translation by selecting Spanish language', function() {
- cy.get('@userPreferencesGeneralTab')
- .click()
- .should('have.class', 'active');
+ cy.changePreferencesTab('@userPreferencesGeneralTab');
+ cy.get('@userPreferencesGeneralTab').should('have.class', 'active');
// Language dropdown should be displayed
cy.get('#language-select').should('be.visible');
@@ -65,14 +64,14 @@ describe('OHIF User Preferences', () => {
cy.get('[data-cy="options-menu"]').click();
});
- it('checks if user can cancel the language selection and application will be in English', function() {
+ it('checks if user can cancel the language selection and application will be in "English (USA)"', function() {
// Set language to English and save
- cy.setLanguage('English');
+ cy.setLanguage('English (USA)');
// Set language to Spanish and cancel
cy.setLanguage('Spanish', false);
- // Header should be kept in English
+ // Header should be kept in "English (USA)"
cy.get('.research-use')
.scrollIntoView()
.should('have.text', 'INVESTIGATIONAL USE ONLY');
@@ -93,7 +92,7 @@ describe('OHIF User Preferences', () => {
cy.get('[data-cy="options-menu"]').click();
});
- it('checks if user can restore to default the language selection and application will be in English', function() {
+ it('checks if user can restore to default the language selection and application will be in "English (USA)"', function() {
// Set language to Spanish
cy.setLanguage('Spanish');
@@ -101,7 +100,7 @@ describe('OHIF User Preferences', () => {
cy.openPreferences();
// Go to general tab
- cy.get('@userPreferencesGeneralTab').click();
+ cy.changePreferencesTab('@userPreferencesGeneralTab');
cy.get('@restoreBtn')
.scrollIntoView()
@@ -112,12 +111,12 @@ describe('OHIF User Preferences', () => {
.scrollIntoView()
.click();
- // Header should be in English
+ // Header should be in "English (USA)"
cy.get('.research-use')
.scrollIntoView()
.should('have.text', 'INVESTIGATIONAL USE ONLY');
- // Options menu should be in English
+ // Options menu should be in "English (USA)"
cy.get('[data-cy="options-menu"]')
.should('have.text', 'Options')
.click();
@@ -135,9 +134,8 @@ describe('OHIF User Preferences', () => {
it('checks if Preferences set in Study List Page will be consistent on Viewer Page', function() {
// Go go hotkeys tab
- cy.get('@userPreferencesHotkeysTab')
- .click()
- .should('have.class', 'active');
+ cy.changePreferencesTab('@userPreferencesHotkeysTab');
+ cy.get('@userPreferencesHotkeysTab').should('have.class', 'active');
// Set new hotkey for 'Rotate Right' function
cy.setNewHotkeyShortcutOnUserPreferencesModal('Rotate Right', '{shift}Q');
@@ -150,7 +148,7 @@ describe('OHIF User Preferences', () => {
cy.openPreferences();
// Go to General tab
- cy.get('@userPreferencesGeneralTab').click();
+ cy.changePreferencesTab('@userPreferencesGeneralTab');
// Set language to Spanish
cy.setLanguage('Spanish');
@@ -221,9 +219,8 @@ describe('OHIF User Preferences', () => {
});
it('checks translation by selecting Spanish language', function() {
- cy.get('@userPreferencesGeneralTab')
- .click()
- .should('have.class', 'active');
+ cy.changePreferencesTab('@userPreferencesGeneralTab');
+ cy.get('@userPreferencesGeneralTab').should('have.class', 'active');
// Language dropdown should be displayed
cy.get('#language-select').should('be.visible');
@@ -256,14 +253,14 @@ describe('OHIF User Preferences', () => {
cy.get('[data-cy="options-menu"]').click();
});
- it('checks if user can cancel the language selection and application will be in English', function() {
+ it('checks if user can cancel the language selection and application will be in "English (USA)"', function() {
// Set language to English and save
- cy.setLanguage('English');
+ cy.setLanguage('English (USA)');
// Set language to Spanish and cancel
cy.setLanguage('Spanish', false);
- // Header should be kept in English
+ // Header should be kept in "English (USA)"
cy.get('.research-use')
.scrollIntoView()
.should('have.text', 'INVESTIGATIONAL USE ONLY');
@@ -283,10 +280,9 @@ describe('OHIF User Preferences', () => {
cy.get('[data-cy="options-menu"]').click();
});
- it('checks if user can restore to default the language selection and application will be in English', function() {
- cy.get('@userPreferencesGeneralTab')
- .click()
- .should('have.class', 'active');
+ it('checks if user can restore to default the language selection and application will be in "English (USA)', function() {
+ cy.changePreferencesTab('@userPreferencesGeneralTab');
+ cy.get('@userPreferencesGeneralTab').should('have.class', 'active');
// Language dropdown should be displayed
cy.get('#language-select').should('be.visible');
@@ -298,7 +294,7 @@ describe('OHIF User Preferences', () => {
cy.openPreferences();
// Go to general tab
- cy.get('@userPreferencesGeneralTab').click();
+ cy.changePreferencesTab('@userPreferencesGeneralTab');
cy.get('@restoreBtn')
.scrollIntoView()
@@ -308,12 +304,12 @@ describe('OHIF User Preferences', () => {
.scrollIntoView()
.click();
- // Header should be in English
+ // Header should be in "English (USA)""
cy.get('.research-use')
.scrollIntoView()
.should('have.text', 'INVESTIGATIONAL USE ONLY');
- // Options menu should be in English
+ // Options menu should be in "English (USA)"
cy.get('[data-cy="options-menu"]')
.should('have.text', 'Options')
.click();
@@ -330,9 +326,8 @@ describe('OHIF User Preferences', () => {
it('checks new hotkeys for "Rotate Right" and "Rotate Left"', function() {
// Go go hotkeys tab
- cy.get('@userPreferencesHotkeysTab')
- .click()
- .should('have.class', 'active');
+ cy.changePreferencesTab('@userPreferencesHotkeysTab');
+ cy.get('@userPreferencesHotkeysTab').should('have.class', 'active');
// Set new hotkey for 'Rotate Right' function
cy.setNewHotkeyShortcutOnUserPreferencesModal(
@@ -360,24 +355,17 @@ describe('OHIF User Preferences', () => {
});
it('checks new hotkeys for "Next" and "Previous" Image on Viewport', function() {
- // Go go hotkeys tab
- cy.get('@userPreferencesHotkeysTab')
- .click()
- .should('have.class', 'active');
-
- // Set new hotkey for 'Next Image Viewport' function
+ // Update hotkeys for 'Next/Previous Viewport'
+ cy.changePreferencesTab('@userPreferencesHotkeysTab');
+ cy.get('@userPreferencesHotkeysTab').should('have.class', 'active');
cy.setNewHotkeyShortcutOnUserPreferencesModal(
- 'Next Image Viewport',
+ 'Next Viewport',
'{shift}{rightarrow}'
);
-
- // Set new hotkey for 'Previous Image Viewport' function
cy.setNewHotkeyShortcutOnUserPreferencesModal(
- 'Previous Image Viewport',
+ 'Previous Viewport',
'{shift}{leftarrow}'
);
-
- // Save new hotkeys
cy.get('@saveBtn')
.scrollIntoView()
.click();
@@ -386,20 +374,25 @@ describe('OHIF User Preferences', () => {
cy.setLayout(3, 1);
cy.waitViewportImageLoading();
- // Rotate Right and Invert colors on Viewport #1
- cy.get('body').type('RI');
- // Check that image was rotated
+ // Reset, Rotate Right and Invert colors on Viewport #1
+ cy.get('body').type(' ');
+ cy.get('body').type('r');
+ cy.get('body').type('i');
+
+ // Shift active viewport to next
+ // Reset, Rotate Left and Invert colors on Viewport #2
+ cy.get('body').type('{shift}{rightarrow}');
+ cy.get('body').type(' ');
+ cy.get('body').type('l');
+ cy.get('body').type('i');
+
+ // Verify 1st viewport was rotated
cy.get('@viewportInfoMidTop').should('contains.text', 'R');
- //Move to Next Viewport
- cy.get('body').type('{shift}{rightarrow}');
- // Rotate Left and Invert colors on Viewport #2
- cy.get('body').type('LI');
- // Get overlay information from viewport #2
+ // Verify 2nd viewport was rotated
cy.get(
':nth-child(2) > .viewport-wrapper > .viewport-element > .ViewportOrientationMarkers.noselect > .top-mid.orientation-marker'
).as('viewport2InfoMidTop');
- // Check that image was rotated
cy.get('@viewport2InfoMidTop').should('contains.text', 'P');
//Move to Previous Viewport
@@ -417,24 +410,18 @@ describe('OHIF User Preferences', () => {
it('checks error message when duplicated hotkeys are inserted', function() {
// Go go hotkeys tab
- cy.get('@userPreferencesHotkeysTab').click();
+ cy.changePreferencesTab('@userPreferencesHotkeysTab');
// Set duplicated hotkey for 'Rotate Right' function
- cy.setNewHotkeyShortcutOnUserPreferencesModal(
- 'Rotate Right',
- '{rightarrow}'
- );
+ cy.setNewHotkeyShortcutOnUserPreferencesModal('Rotate Right', '{i}');
// Check error message
- cy.get('.HotKeysPreferences').within(() => {
+ cy.get('.HotkeysPreferences').within(() => {
cy.contains('Rotate Right') // label we're looking for
.parent()
.find('.errorMessage')
.as('errorMsg')
- .should(
- 'have.text',
- '"Next Image Viewport" is already using the "right" shortcut.'
- );
+ .should('have.text', '"Invert" is already using the "i" shortcut.');
});
//Cancel hotkeys
cy.get('@cancelBtn')
@@ -444,13 +431,13 @@ describe('OHIF User Preferences', () => {
it('checks error message when invalid hotkey is inserted', function() {
// Go go hotkeys tab
- cy.get('@userPreferencesHotkeysTab').click();
+ cy.changePreferencesTab('@userPreferencesHotkeysTab');
// Set invalid hotkey for 'Rotate Right' function
cy.setNewHotkeyShortcutOnUserPreferencesModal('Rotate Right', '{ctrl}Z');
// Check error message
- cy.get('.HotKeysPreferences').within(() => {
+ cy.get('.HotkeysPreferences').within(() => {
cy.contains('Rotate Right') // label we're looking for
.parent()
.find('.errorMessage')
@@ -466,12 +453,12 @@ describe('OHIF User Preferences', () => {
it('checks error message when only modifier keys are inserted', function() {
// Go go hotkeys tab
- cy.get('@userPreferencesHotkeysTab').click();
+ cy.changePreferencesTab('@userPreferencesHotkeysTab');
// Set invalid modifier key: ctrl
cy.setNewHotkeyShortcutOnUserPreferencesModal('Zoom Out', '{ctrl}');
// Check error message
- cy.get('.HotKeysPreferences').within(() => {
+ cy.get('.HotkeysPreferences').within(() => {
cy.contains('Zoom Out') // label we're looking for
.parent()
.find('.errorMessage')
@@ -506,7 +493,7 @@ describe('OHIF User Preferences', () => {
it('checks if user can cancel changes made on User Preferences Hotkeys tab', function() {
// Go go hotkeys tab
- cy.get('@userPreferencesHotkeysTab').click();
+ cy.changePreferencesTab('@userPreferencesHotkeysTab');
// Set new hotkey for 'Rotate Right' function
cy.setNewHotkeyShortcutOnUserPreferencesModal(
@@ -523,7 +510,7 @@ describe('OHIF User Preferences', () => {
cy.openPreferences();
//Check that hotkey for 'Rotate Right' function was not changed
- cy.get('.HotKeysPreferences').within(() => {
+ cy.get('.HotkeysPreferences').within(() => {
cy.contains('Rotate Right') // label we're looking for
.parent()
.find('input')
@@ -534,7 +521,7 @@ describe('OHIF User Preferences', () => {
it('checks if user can reset to default values on User Preferences Hotkeys tab', function() {
// Go go hotkeys tab
- cy.get('@userPreferencesHotkeysTab').click();
+ cy.changePreferencesTab('@userPreferencesHotkeysTab');
// Set new hotkey for 'Rotate Right' function
cy.setNewHotkeyShortcutOnUserPreferencesModal(
@@ -559,7 +546,7 @@ describe('OHIF User Preferences', () => {
cy.openPreferences();
//Check that hotkey for 'Rotate Right' function was not changed
- cy.get('.HotKeysPreferences').within(() => {
+ cy.get('.HotkeysPreferences').within(() => {
cy.contains('Rotate Right') // label we're looking for
.parent()
.find('input')
diff --git a/platform/viewer/cypress/integration/visual-regression/PercyCheckOHIFUserPreferences.spec.js b/platform/viewer/cypress/integration/visual-regression/PercyCheckOHIFUserPreferences.spec.js
index 4bf066add..5c6038450 100644
--- a/platform/viewer/cypress/integration/visual-regression/PercyCheckOHIFUserPreferences.spec.js
+++ b/platform/viewer/cypress/integration/visual-regression/PercyCheckOHIFUserPreferences.spec.js
@@ -21,9 +21,8 @@ describe('Visual Regression - OHIF User Preferences', () => {
});
it('checks translation by selecting Spanish language', function() {
- cy.get('@userPreferencesGeneralTab')
- .click()
- .should('have.class', 'active');
+ cy.changePreferencesTab('@userPreferencesGeneralTab');
+ cy.get('@userPreferencesGeneralTab').should('have.class', 'active');
// Language dropdown should be displayed
cy.get('#language-select').should('be.visible');
@@ -76,9 +75,8 @@ describe('Visual Regression - OHIF User Preferences', () => {
});
it('checks translation by selecting Spanish language', function() {
- cy.get('@userPreferencesGeneralTab')
- .click()
- .should('have.class', 'active');
+ cy.changePreferencesTab('@userPreferencesGeneralTab');
+ cy.get('@userPreferencesGeneralTab').should('have.class', 'active');
// Visual comparison
cy.percyCanvasSnapshot(
@@ -102,9 +100,8 @@ describe('Visual Regression - OHIF User Preferences', () => {
});
it('checks if user can restore to default the language selection and application will be in English', function() {
- cy.get('@userPreferencesGeneralTab')
- .click()
- .should('have.class', 'active');
+ cy.changePreferencesTab('@userPreferencesGeneralTab');
+ cy.get('@userPreferencesGeneralTab').should('have.class', 'active');
// Set language to Spanish
cy.setLanguage('Spanish');
@@ -113,7 +110,7 @@ describe('Visual Regression - OHIF User Preferences', () => {
cy.openPreferences();
// Go to general tab
- cy.get('@userPreferencesGeneralTab').click();
+ cy.changePreferencesTab('@userPreferencesGeneralTab');
cy.get('@restoreBtn')
.scrollIntoView()
@@ -139,9 +136,8 @@ describe('Visual Regression - OHIF User Preferences', () => {
it('checks new hotkeys for "Next" and "Previous" Image on Viewport', function() {
// Go go hotkeys tab
- cy.get('@userPreferencesHotkeysTab')
- .click()
- .should('have.class', 'active');
+ cy.changePreferencesTab('@userPreferencesHotkeysTab');
+ cy.get('@userPreferencesHotkeysTab').should('have.class', 'active');
// Set new hotkey for 'Next Image Viewport' function
cy.setNewHotkeyShortcutOnUserPreferencesModal(
diff --git a/platform/viewer/cypress/support/aliases.js b/platform/viewer/cypress/support/aliases.js
index 0547e99d1..7a9809f0e 100644
--- a/platform/viewer/cypress/support/aliases.js
+++ b/platform/viewer/cypress/support/aliases.js
@@ -84,7 +84,12 @@ export function initPreferencesModalAliases() {
cy.get('.OHIFModal').as('preferencesModal');
cy.get('[data-cy="hotkeys"]').as('userPreferencesHotkeysTab');
cy.get('[data-cy="general"]').as('userPreferencesGeneralTab');
- cy.get('[data-cy="reset-default-btn"]').as('restoreBtn');
- cy.get('[data-cy="cancel-btn"]').as('cancelBtn');
- cy.get('[data-cy="save-btn"]').as('saveBtn');
+ initPreferencesModalFooterBtnAliases();
+}
+
+//Creating aliases for User Preferences modal
+export function initPreferencesModalFooterBtnAliases() {
+ cy.get('.active [data-cy="reset-default-btn"]').as('restoreBtn');
+ cy.get('.active [data-cy="cancel-btn"]').as('cancelBtn');
+ cy.get('.active [data-cy="save-btn"]').as('saveBtn');
}
diff --git a/platform/viewer/cypress/support/commands.js b/platform/viewer/cypress/support/commands.js
index 6284740aa..4b8aeeaa7 100644
--- a/platform/viewer/cypress/support/commands.js
+++ b/platform/viewer/cypress/support/commands.js
@@ -9,6 +9,7 @@ import {
initStudyListAliasesOnDesktop,
initStudyListAliasesOnTablet,
initPreferencesModalAliases,
+ initPreferencesModalFooterBtnAliases,
} from './aliases.js';
// ***********************************************
@@ -169,13 +170,13 @@ Cypress.Commands.add(
);
Cypress.Commands.add('expectMinimumThumbnails', (seriesToWait = 1) => {
- cy.get('[data-cy=thumbnail-list]', { timeout: 20000 }).should($itemList => {
+ cy.get('[data-cy=thumbnail-list]', { timeout: 50000 }).should($itemList => {
expect($itemList.length >= seriesToWait).to.be.true;
});
});
//Command to wait DICOM image to load into the viewport
-Cypress.Commands.add('waitDicomImage', (timeout = 20000) => {
+Cypress.Commands.add('waitDicomImage', (timeout = 50000) => {
const loaded = cy.isPageLoaded();
if (loaded) {
@@ -441,6 +442,12 @@ Cypress.Commands.add('openPreferences', () => {
});
});
+Cypress.Commands.add('changePreferencesTab', tabAlias => {
+ cy.initPreferencesModalAliases();
+ cy.get(tabAlias).click();
+ initPreferencesModalFooterBtnAliases();
+});
+
Cypress.Commands.add('resetUserHoktkeyPreferences', () => {
// Open User Preferences modal
cy.openPreferences();
@@ -457,7 +464,7 @@ Cypress.Commands.add(
(function_label, shortcut) => {
// Within scopes all `.get` and `.contains` to within the matched elements
// dom instead of checking from document
- cy.get('.HotKeysPreferences')
+ cy.get('.HotkeysPreferences')
.within(() => {
cy.contains(function_label) // label we're looking for
.parent()
@@ -487,16 +494,16 @@ Cypress.Commands.add('setLanguage', (language, save = true) => {
.click()
.should('have.class', 'active');
+ initPreferencesModalFooterBtnAliases();
+
// Language dropdown should be displayed
cy.get('#language-select').should('be.visible');
// Select Language and Save/Cancel
- cy.get('#language-select')
- .select(language)
- .then(() => {
- const toClick = save ? '@saveBtn' : '@cancelBtn';
- cy.get(toClick)
- .scrollIntoView()
- .click();
- });
+ cy.get('#language-select').select(language);
+
+ const toClick = save ? '@saveBtn' : '@cancelBtn';
+ cy.get(toClick)
+ .scrollIntoView()
+ .click();
});
diff --git a/platform/viewer/public/config/default.js b/platform/viewer/public/config/default.js
index 492294bf7..fc74100b3 100644
--- a/platform/viewer/public/config/default.js
+++ b/platform/viewer/public/config/default.js
@@ -25,12 +25,12 @@ window.config = {
// ~ Global
{
commandName: 'incrementActiveViewport',
- label: 'Next Image Viewport',
+ label: 'Next Viewport',
keys: ['right'],
},
{
commandName: 'decrementActiveViewport',
- label: 'Previous Image Viewport',
+ label: 'Previous Viewport',
keys: ['left'],
},
// Supported Keys: https://craig.is/killing/mice
diff --git a/platform/viewer/public/config/demo.js b/platform/viewer/public/config/demo.js
index 72272ed24..0db0a2bac 100644
--- a/platform/viewer/public/config/demo.js
+++ b/platform/viewer/public/config/demo.js
@@ -20,12 +20,12 @@ window.config = {
hotkeys: [
{
commandName: 'incrementActiveViewport',
- label: 'Next Image Viewport',
+ label: 'Next Viewport',
keys: ['right'],
},
{
commandName: 'decrementActiveViewport',
- label: 'Previous Image Viewport',
+ label: 'Previous Viewport',
keys: ['left'],
},
{ commandName: 'rotateViewportCW', label: 'Rotate Right', keys: ['r'] },
diff --git a/platform/viewer/public/config/netlify.js b/platform/viewer/public/config/netlify.js
index 41e969760..b46bd8fb6 100644
--- a/platform/viewer/public/config/netlify.js
+++ b/platform/viewer/public/config/netlify.js
@@ -20,12 +20,12 @@ window.config = {
// ~ Global
{
commandName: 'incrementActiveViewport',
- label: 'Next Image Viewport',
+ label: 'Next Viewport',
keys: ['right'],
},
{
commandName: 'decrementActiveViewport',
- label: 'Previous Image Viewport',
+ label: 'Previous Viewport',
keys: ['left'],
},
// Supported Keys: https://craig.is/killing/mice
diff --git a/platform/viewer/src/App.js b/platform/viewer/src/App.js
index a835fbfa1..6819838ff 100644
--- a/platform/viewer/src/App.js
+++ b/platform/viewer/src/App.js
@@ -53,7 +53,6 @@ import store from './store';
import WhiteLabellingContext from './context/WhiteLabellingContext';
import UserManagerContext from './context/UserManagerContext';
import AppContext from './context/AppContext';
-const { setUserPreferences } = reduxOHIF.actions;
/** ~~~~~~~~~~~~~ Application Setup */
const commandsManagerConfig = {
@@ -63,8 +62,8 @@ const commandsManagerConfig = {
/** Managers */
const commandsManager = new CommandsManager(commandsManagerConfig);
-const hotkeysManager = new HotkeysManager(commandsManager);
const servicesManager = new ServicesManager();
+const hotkeysManager = new HotkeysManager(commandsManager, servicesManager);
let extensionManager;
/** ~~~~~~~~~~~~~ End Application Setup */
@@ -116,7 +115,7 @@ class App extends Component {
const {
servers,
- hotkeys,
+ hotkeys: appConfigHotkeys,
cornerstoneExtensionConfig,
extensions,
oidc,
@@ -139,7 +138,7 @@ class App extends Component {
* Must run after extension commands are registered
* if there is no hotkeys from localStorage set up from config.
*/
- _initHotkeys(hotkeys);
+ _initHotkeys(appConfigHotkeys);
_initServers(servers);
initWebWorkers();
}
@@ -263,30 +262,27 @@ function _initExtensions(extensions, cornerstoneExtensionConfig, appConfig) {
extensionManager.registerExtensions(mergedExtensions);
}
-function _initHotkeys(hotkeys) {
- const { hotkeyDefinitions = {} } = store.getState().preferences || {};
- let updateStore = false;
- let hotkeysToUse = hotkeyDefinitions;
+/**
+ *
+ * @param {Object} appConfigHotkeys - Default hotkeys, as defined by app config
+ */
+function _initHotkeys(appConfigHotkeys) {
+ // TODO: Use something more resilient
+ // TODO: Mozilla has a special library for this
+ const userPreferredHotkeys = JSON.parse(
+ localStorage.getItem('hotkey-definitions') || '{}'
+ );
- if (!Object.keys(hotkeyDefinitions).length) {
- hotkeysToUse = hotkeys;
- updateStore = true;
+ // TODO: hotkeysManager.isValidDefinitionObject(/* */)
+ const hasUserPreferences =
+ userPreferredHotkeys && Object.keys(userPreferredHotkeys).length > 0;
+ if (hasUserPreferences) {
+ hotkeysManager.setHotkeys(userPreferredHotkeys);
+ } else {
+ hotkeysManager.setHotkeys(appConfigHotkeys);
}
- if (hotkeysToUse) {
- hotkeysManager.setHotkeys(hotkeysToUse);
-
- /* Set hotkeys default based on app config. */
- hotkeysManager.setDefaultHotKeys(hotkeys);
-
- if (updateStore) {
- const { hotkeyDefinitions } = hotkeysManager;
- const windowLevelData = {};
- store.dispatch(
- setUserPreferences({ windowLevelData, hotkeyDefinitions })
- );
- }
- }
+ hotkeysManager.setDefaultHotKeys(appConfigHotkeys);
}
function _initServers(servers) {
diff --git a/platform/viewer/src/components/Header/Header.js b/platform/viewer/src/components/Header/Header.js
index 5383f1f75..8fa2a6937 100644
--- a/platform/viewer/src/components/Header/Header.js
+++ b/platform/viewer/src/components/Header/Header.js
@@ -1,10 +1,12 @@
import React, { useState, useEffect } from 'react';
import { Link, withRouter } from 'react-router-dom';
import { withTranslation } from 'react-i18next';
+
import PropTypes from 'prop-types';
-import ConnectedUserPreferencesForm from '../../connectedComponents/ConnectedUserPreferencesForm';
import { Dropdown, AboutContent, withModal } from '@ohif/ui';
+
+import { UserPreferences } from './../UserPreferences';
import OHIFLogo from '../OHIFLogo/OHIFLogo.js';
import './Header.css';
@@ -42,7 +44,7 @@ function Header(props) {
},
onClick: () =>
show({
- content: ConnectedUserPreferencesForm,
+ content: UserPreferences,
title: t('User Preferences'),
}),
},
diff --git a/platform/viewer/src/components/UserPreferences/GeneralPreferences.js b/platform/viewer/src/components/UserPreferences/GeneralPreferences.js
new file mode 100644
index 000000000..67eb5000d
--- /dev/null
+++ b/platform/viewer/src/components/UserPreferences/GeneralPreferences.js
@@ -0,0 +1,72 @@
+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 (
+
+
+
+
+ );
+}
+
+GeneralPreferences.propTypes = {
+ onClose: PropTypes.func,
+};
+
+export { GeneralPreferences };
diff --git a/platform/viewer/src/components/UserPreferences/GeneralPreferences.styl b/platform/viewer/src/components/UserPreferences/GeneralPreferences.styl
new file mode 100644
index 000000000..795009555
--- /dev/null
+++ b/platform/viewer/src/components/UserPreferences/GeneralPreferences.styl
@@ -0,0 +1,18 @@
+.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;
diff --git a/platform/viewer/src/components/UserPreferences/HotkeysPreferences.js b/platform/viewer/src/components/UserPreferences/HotkeysPreferences.js
new file mode 100644
index 000000000..1800ee89c
--- /dev/null
+++ b/platform/viewer/src/components/UserPreferences/HotkeysPreferences.js
@@ -0,0 +1,201 @@
+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 (
+
+
+ {hasHotkeys ? (
+
+ {splitedHotkeys.map((hotkeys, index) => {
+ return (
+
+
+ {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 (
+
+
{label}
+
+
+ {errorMessage}
+
+
+ );
+ })}
+
+ );
+ })}
+
+ ) : (
+ 'Hotkeys definitions is empty'
+ )}
+
+
+
+ );
+}
+
+HotkeysPreferences.propTypes = {
+ onClose: PropTypes.func,
+};
+
+export { HotkeysPreferences };
diff --git a/platform/viewer/src/components/UserPreferences/HotkeysPreferences.styl b/platform/viewer/src/components/UserPreferences/HotkeysPreferences.styl
new file mode 100644
index 000000000..e55870a28
--- /dev/null
+++ b/platform/viewer/src/components/UserPreferences/HotkeysPreferences.styl
@@ -0,0 +1,73 @@
+.HotkeysPreferences
+ display: flex
+ padding: 20px
+
+ .errorMessage
+ color: var(--state-error-text)
+ font-size: 10px
+ text-transform: uppercase
+
+ .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
+
+ .hotkeyInput
+ 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
+
+ .hotkeyInput:focus
+ border-color: var(--active-color)
+ background-color: var(--ui-gray-dark)
+ box-shadow: 0 0 0 2px var(--active-color) !important
+ outline: 0
+
+ .hotkeyLabel
+ padding: 5px 15px 5px 0
+ text-align: right
+ flex-basis: 0
+ flex-grow: 1.5
+
+ .stateError
+ .hotkeyInput
+ background-color: var(--state-error)
diff --git a/platform/viewer/src/components/UserPreferences/UserPreferences.js b/platform/viewer/src/components/UserPreferences/UserPreferences.js
new file mode 100644
index 000000000..dda64ba5d
--- /dev/null
+++ b/platform/viewer/src/components/UserPreferences/UserPreferences.js
@@ -0,0 +1,43 @@
+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';
+
+const tabs = [
+ {
+ name: 'Hotkeys',
+ Component: HotkeysPreferences,
+ customProps: {},
+ hidden: false,
+ },
+ {
+ name: 'General',
+ Component: GeneralPreferences,
+ customProps: {},
+ hidden: false,
+ },
+ {
+ name: 'Window Level',
+ Component: WindowLevelPreferences,
+ customProps: {},
+ hidden: true,
+ },
+];
+
+function UserPreferences({ hide }) {
+ const customProps = {
+ onClose: hide,
+ };
+ return ;
+}
+
+UserPreferences.propTypes = {
+ hide: PropTypes.func,
+};
+
+export { UserPreferences };
diff --git a/platform/viewer/src/components/UserPreferences/WindowLevelPreferences.js b/platform/viewer/src/components/UserPreferences/WindowLevelPreferences.js
new file mode 100644
index 000000000..e3e0cc547
--- /dev/null
+++ b/platform/viewer/src/components/UserPreferences/WindowLevelPreferences.js
@@ -0,0 +1,32 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import { TabFooter } from '@ohif/ui';
+import { useTranslation } from 'react-i18next';
+
+function WindowLevelPreferences({ onClose }) {
+ const { t } = useTranslation('UserPreferencesModal');
+ const onResetPreferences = () => {};
+ const onSave = () => {};
+ const hasErrors = false;
+
+ return (
+
+ Component content: {name}
+ TDB!
+
+
+ );
+}
+
+WindowLevelPreferences.propTypes = {
+ onClose: PropTypes.func,
+};
+
+export { WindowLevelPreferences };
diff --git a/platform/ui/src/components/userPreferencesForm/hotKeysConfig.js b/platform/viewer/src/components/UserPreferences/hotkeysConfig.js
similarity index 73%
rename from platform/ui/src/components/userPreferencesForm/hotKeysConfig.js
rename to platform/viewer/src/components/UserPreferences/hotkeysConfig.js
index 9a0b2a2af..23d286b19 100644
--- a/platform/ui/src/components/userPreferencesForm/hotKeysConfig.js
+++ b/platform/viewer/src/components/UserPreferences/hotkeysConfig.js
@@ -2,7 +2,9 @@ const range = (start, end) => {
return new Array(end - start).fill().map((d, i) => i + start);
};
-export const disallowedCombinations = {
+export const MODIFIER_KEYS = ['ctrl', 'alt', 'shift'];
+
+export const DISALLOWED_COMBINATIONS = {
'': [],
alt: ['space'],
shift: [],
@@ -34,18 +36,7 @@ export const disallowedCombinations = {
'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 = {
+export const SPECIAL_KEYS = {
8: 'backspace',
9: 'tab',
13: 'return',
diff --git a/platform/viewer/src/components/UserPreferences/hotkeysValidators.js b/platform/viewer/src/components/UserPreferences/hotkeysValidators.js
new file mode 100644
index 000000000..cfa7bb27f
--- /dev/null
+++ b/platform/viewer/src/components/UserPreferences/hotkeysValidators.js
@@ -0,0 +1,97 @@
+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 };
diff --git a/platform/viewer/src/components/UserPreferences/index.js b/platform/viewer/src/components/UserPreferences/index.js
new file mode 100644
index 000000000..d80719bd7
--- /dev/null
+++ b/platform/viewer/src/components/UserPreferences/index.js
@@ -0,0 +1 @@
+export { UserPreferences } from './UserPreferences';
diff --git a/platform/viewer/src/connectedComponents/ConnectedUserPreferencesForm.js b/platform/viewer/src/connectedComponents/ConnectedUserPreferencesForm.js
deleted file mode 100644
index 26c6a237d..000000000
--- a/platform/viewer/src/connectedComponents/ConnectedUserPreferencesForm.js
+++ /dev/null
@@ -1,58 +0,0 @@
-import { connect } from 'react-redux';
-import { UserPreferencesForm } from '@ohif/ui';
-import OHIF from '@ohif/core';
-import i18n from '@ohif/i18n';
-
-import { hotkeysManager } from '../App.js';
-
-const { setUserPreferences } = OHIF.redux.actions;
-
-const mapStateToProps = (state, ownProps) => {
- const { defaultLanguage } = i18n;
- const { hotkeyDefinitions, windowLevelData = {}, generalPreferences } =
- state.preferences || {};
- const { hotkeyDefaults } = hotkeysManager;
-
- return {
- onClose: ownProps.hide,
- windowLevelData,
- hotkeyDefinitions,
- generalPreferences,
- hotkeysManager,
- hotkeyDefaults,
- defaultLanguage,
- };
-};
-
-const mapDispatchToProps = (dispatch, ownProps) => {
- return {
- onSave: ({ windowLevelData, hotkeyDefinitions, generalPreferences }) => {
- // TODO improve this strategy on windowLevel implementation
- hotkeysManager.setHotkeys(hotkeyDefinitions);
-
- const { language } = generalPreferences;
-
- // set new language
- i18n.changeLanguage(language);
-
- if (ownProps.hide) {
- ownProps.hide();
- }
-
- dispatch(
- setUserPreferences({
- windowLevelData,
- hotkeyDefinitions,
- generalPreferences,
- })
- );
- },
- };
-};
-
-const ConnectedUserPreferencesForm = connect(
- mapStateToProps,
- mapDispatchToProps
-)(UserPreferencesForm);
-
-export default ConnectedUserPreferencesForm;