From 2f30e7a821a238144c49c56f37d8e5565540b4bd Mon Sep 17 00:00:00 2001 From: Rodrigo Antinarelli Date: Wed, 12 Feb 2020 17:35:04 -0300 Subject: [PATCH] fix: Combined Hotkeys for special characters (#1233) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: Combined Hotkeys for special characters * add record method to hotkey manager * fix record plugin * remove unused component * add record to modal props * rename record method * replace handlers to use hotkeyRecord * fix combined keys * change expected result count from 18 to 17 * autoformat * Remove duplicate test, that was testing the wrong things; fix label; update configs * Revert "Remove duplicate test, that was testing the wrong things; fix label; update configs" This reverts commit 4292f4fe67351962d61cae623b920dcbd87dd71d. * Fix the record plugin's registration * fix exposed record method usage * adding logging for info level items * Hotkey definitions don't need to be globally reactive; use localstorage/appconfig as sources of truth; not redux * Tidy up test * Remove unused code from UserPreferencesForm * Log info when we run a command * fix hotkey preference restore * use application configured hotkeys if there are no user preferred * Avoid logging circular ref * Fix callouts * Fix small issue with array * Fix langua issue after refactor and merge * Refactor on recordCurrentCombo as Rodrigo did before * Separating components in 2 files * WIP Refactor to simplify the user preferences and move into each form the save and controll functionalities * Remove context * Remove unused import * Initial work on Field treatment * Refactor General preferences * Small refactor removing type from HotkeyField * small update on style * Refactor and layout fixed * Make hotkeys preferences working with old hotkeys row * Move error handling out of hotkey row/input component * WIP custom form * Moving validation function to component * Exposing hotkeyRecord as it does not depend on HotkeyManager Class * Making hotkeyField as much detached possible from parent component * Small refactors * Refactor on user preferences * Clean up into the changes * Small fix to let save working * Style finish * move about docs into about folder * Fix double tap on single keys * Style refactor * Remove log * Fix log issues on unit tests * Fix unit test breaking on ohif/core index * Fixing hotkeys unpause unit test issue * Rename file to adopt lowercase * Rename file to adopt lowercase * Fixing callouts * Big refactor miving some of the components into viewer and creating small components into ohif/ui * Typo on folder name * Updating ohif ui docs * Remove comments * Fix binding of combo keys * Fix some cypress tests failures * Fixing onCancel button * Fixing e2e tests * Small style update * Fixing unit tests failing after fix issue * Remove some not used code * Remove left over after debug * Adding prevent default on hotkeys events * Fixinf existing hotkeys validator with 3 keys pressed * Exposing hotkeys as root level on ohif-core * Clean up * Exposing all availableLanguages with labels and fixing an issue on language switcher * Fixing e2e cypress tests * Preveinting some simple errors * Treating error once we try to set hotkey definitions * Adding ui notification on setHotkeys errors * Implementing a service queue request to hold until functions are implemented * Making sure toFixed is only called on Numbers Co-authored-by: Danny Brown Co-authored-by: Gustavo André Lelis --- platform/core/src/__mocks__/log.js | 1 + platform/core/src/classes/HotkeysManager.js | 69 ++- .../core/src/classes/HotkeysManager.test.js | 29 +- platform/core/src/classes/hotkeys/index.js | 17 - platform/core/src/index.js | 4 +- platform/core/src/index.test.js | 1 + platform/core/src/measurements/tools/angle.js | 2 +- .../core/src/measurements/tools/circleRoi.js | 2 +- .../src/measurements/tools/ellipticalRoi.js | 2 +- .../src/measurements/tools/freehandMouse.js | 2 +- .../core/src/measurements/tools/length.js | 2 +- .../src/measurements/tools/rectangleRoi.js | 2 +- .../core/src/redux/reducers/preferences.js | 6 - .../services/UINotificationService/index.js | 13 +- platform/core/src/utils/hotkeys/index.js | 8 + .../{classes => utils}/hotkeys/pausePlugin.js | 0 .../hotkeys/recordPlugin.js | 34 +- platform/core/src/utils/index.js | 3 + platform/core/src/utils/index.test.js | 1 + .../i18n/src/getAvailableLanguagesInfo.js | 70 +++ platform/i18n/src/index.js | 3 + .../aboutContent}/__docs__/about.mdx | 0 .../src/components/customForm/HotkeyField.js | 82 ++++ .../ui/src/components/customForm/index.js | 1 + platform/ui/src/components/index.js | 15 +- .../languageSwitcher/LanguageSwitcher.js | 24 +- .../src/components/languageSwitcher/index.js | 2 +- .../components/tabComponents/TabComponents.js | 105 ++++ .../tabComponents/TabComponents.styl | 21 + .../src/components/tabComponents/TabFooter.js | 54 +++ .../components/tabComponents/TabFooter.styl | 11 + .../tabComponents/__docs__/tabComponents.mdx | 27 ++ .../components/tabComponents/__docs__/tabs.js | 26 + .../ui/src/components/tabComponents/index.js | 2 + .../userPreferencesForm/GeneralPreferences.js | 57 --- .../userPreferencesForm/HotKeysPreferences.js | 452 ------------------ .../HotKeysPreferences.styl | 12 - .../userPreferencesForm/UserPreferences.js | 132 ----- .../UserPreferencesForm.js | 219 --------- .../UserPreferencesForm.styl | 23 - .../UserPreferencesTabs.js | 107 ----- .../UserPreferencesTabs.styl | 29 -- .../WindowLevelPreferences.js | 132 ----- .../WindowLevelPreferences.styl | 4 - .../__docs__/generalDefaults.js | 14 - .../__docs__/hotkeyDefaults.js | 75 --- .../__docs__/userPreferences.mdx | 54 --- .../__docs__/windowLevelDefaults.js | 13 - .../components/userPreferencesForm/index.js | 4 - platform/ui/src/index.js | 12 +- .../common/OHIFUserPreferences.spec.js | 125 +++-- .../PercyCheckOHIFUserPreferences.spec.js | 22 +- platform/viewer/cypress/support/aliases.js | 11 +- platform/viewer/cypress/support/commands.js | 29 +- platform/viewer/public/config/default.js | 4 +- platform/viewer/public/config/demo.js | 4 +- platform/viewer/public/config/netlify.js | 4 +- platform/viewer/src/App.js | 46 +- .../viewer/src/components/Header/Header.js | 6 +- .../UserPreferences/GeneralPreferences.js | 72 +++ .../UserPreferences/GeneralPreferences.styl | 18 + .../UserPreferences/HotkeysPreferences.js | 201 ++++++++ .../UserPreferences/HotkeysPreferences.styl | 73 +++ .../UserPreferences/UserPreferences.js | 43 ++ .../UserPreferences/WindowLevelPreferences.js | 32 ++ .../UserPreferences/hotkeysConfig.js} | 17 +- .../UserPreferences/hotkeysValidators.js | 97 ++++ .../src/components/UserPreferences/index.js | 1 + .../ConnectedUserPreferencesForm.js | 58 --- 69 files changed, 1211 insertions(+), 1632 deletions(-) delete mode 100644 platform/core/src/classes/hotkeys/index.js create mode 100644 platform/core/src/utils/hotkeys/index.js rename platform/core/src/{classes => utils}/hotkeys/pausePlugin.js (100%) rename platform/core/src/{classes => utils}/hotkeys/recordPlugin.js (89%) create mode 100644 platform/i18n/src/getAvailableLanguagesInfo.js rename platform/ui/src/components/{userPreferencesForm => content/aboutContent}/__docs__/about.mdx (100%) create mode 100644 platform/ui/src/components/customForm/HotkeyField.js create mode 100644 platform/ui/src/components/customForm/index.js create mode 100644 platform/ui/src/components/tabComponents/TabComponents.js create mode 100644 platform/ui/src/components/tabComponents/TabComponents.styl create mode 100644 platform/ui/src/components/tabComponents/TabFooter.js create mode 100644 platform/ui/src/components/tabComponents/TabFooter.styl create mode 100644 platform/ui/src/components/tabComponents/__docs__/tabComponents.mdx create mode 100644 platform/ui/src/components/tabComponents/__docs__/tabs.js create mode 100644 platform/ui/src/components/tabComponents/index.js delete mode 100644 platform/ui/src/components/userPreferencesForm/GeneralPreferences.js delete mode 100644 platform/ui/src/components/userPreferencesForm/HotKeysPreferences.js delete mode 100644 platform/ui/src/components/userPreferencesForm/HotKeysPreferences.styl delete mode 100644 platform/ui/src/components/userPreferencesForm/UserPreferences.js delete mode 100644 platform/ui/src/components/userPreferencesForm/UserPreferencesForm.js delete mode 100644 platform/ui/src/components/userPreferencesForm/UserPreferencesForm.styl delete mode 100644 platform/ui/src/components/userPreferencesForm/UserPreferencesTabs.js delete mode 100644 platform/ui/src/components/userPreferencesForm/UserPreferencesTabs.styl delete mode 100644 platform/ui/src/components/userPreferencesForm/WindowLevelPreferences.js delete mode 100644 platform/ui/src/components/userPreferencesForm/WindowLevelPreferences.styl delete mode 100644 platform/ui/src/components/userPreferencesForm/__docs__/generalDefaults.js delete mode 100644 platform/ui/src/components/userPreferencesForm/__docs__/hotkeyDefaults.js delete mode 100644 platform/ui/src/components/userPreferencesForm/__docs__/userPreferences.mdx delete mode 100644 platform/ui/src/components/userPreferencesForm/__docs__/windowLevelDefaults.js delete mode 100644 platform/ui/src/components/userPreferencesForm/index.js create mode 100644 platform/viewer/src/components/UserPreferences/GeneralPreferences.js create mode 100644 platform/viewer/src/components/UserPreferences/GeneralPreferences.styl create mode 100644 platform/viewer/src/components/UserPreferences/HotkeysPreferences.js create mode 100644 platform/viewer/src/components/UserPreferences/HotkeysPreferences.styl create mode 100644 platform/viewer/src/components/UserPreferences/UserPreferences.js create mode 100644 platform/viewer/src/components/UserPreferences/WindowLevelPreferences.js rename platform/{ui/src/components/userPreferencesForm/hotKeysConfig.js => viewer/src/components/UserPreferences/hotkeysConfig.js} (73%) create mode 100644 platform/viewer/src/components/UserPreferences/hotkeysValidators.js create mode 100644 platform/viewer/src/components/UserPreferences/index.js delete mode 100644 platform/viewer/src/connectedComponents/ConnectedUserPreferencesForm.js 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 ( -
- - - - - - - - - {Object.entries(columnHotKeys).map( - hotkeyDefinitionTuple => ( - - onHotKeyChanged( - hotkeyDefinitionTuple[0], - hotkeyDefinitionTuple[1], - keys - ) - } - onFailureChanged={onErrorChanged} - > - ) - )} - -
FunctionShortcut
-
- ); - }) - : 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 ( - - - - - - - - - - - {Object.keys(tabState).map(objKey => ( - - ))} - -
    PresetDescriptionWindowLevel
    - ); -} - -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 ( +
    +
    +
    Function
    +
    Shortcut
    +
    + {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;