fix: Combined Hotkeys for special characters (#1233)
* fix: Combined Hotkeys for special characters * add record method to hotkey manager * fix record plugin * remove unused component * add record to modal props * rename record method * replace handlers to use hotkeyRecord * fix combined keys * change expected result count from 18 to 17 * autoformat * Remove duplicate test, that was testing the wrong things; fix label; update configs * Revert "Remove duplicate test, that was testing the wrong things; fix label; update configs" This reverts commit 4292f4fe67351962d61cae623b920dcbd87dd71d. * Fix the record plugin's registration * fix exposed record method usage * adding logging for info level items * Hotkey definitions don't need to be globally reactive; use localstorage/appconfig as sources of truth; not redux * Tidy up test * Remove unused code from UserPreferencesForm * Log info when we run a command * fix hotkey preference restore * use application configured hotkeys if there are no user preferred * Avoid logging circular ref * Fix callouts * Fix small issue with array * Fix langua issue after refactor and merge * Refactor on recordCurrentCombo as Rodrigo did before * Separating components in 2 files * WIP Refactor to simplify the user preferences and move into each form the save and controll functionalities * Remove context * Remove unused import * Initial work on Field treatment * Refactor General preferences * Small refactor removing type from HotkeyField * small update on style * Refactor and layout fixed * Make hotkeys preferences working with old hotkeys row * Move error handling out of hotkey row/input component * WIP custom form * Moving validation function to component * Exposing hotkeyRecord as it does not depend on HotkeyManager Class * Making hotkeyField as much detached possible from parent component * Small refactors * Refactor on user preferences * Clean up into the changes * Small fix to let save working * Style finish * move about docs into about folder * Fix double tap on single keys * Style refactor * Remove log * Fix log issues on unit tests * Fix unit test breaking on ohif/core index * Fixing hotkeys unpause unit test issue * Rename file to adopt lowercase * Rename file to adopt lowercase * Fixing callouts * Big refactor miving some of the components into viewer and creating small components into ohif/ui * Typo on folder name * Updating ohif ui docs * Remove comments * Fix binding of combo keys * Fix some cypress tests failures * Fixing onCancel button * Fixing e2e tests * Small style update * Fixing unit tests failing after fix issue * Remove some not used code * Remove left over after debug * Adding prevent default on hotkeys events * Fixinf existing hotkeys validator with 3 keys pressed * Exposing hotkeys as root level on ohif-core * Clean up * Exposing all availableLanguages with labels and fixing an issue on language switcher * Fixing e2e cypress tests * Preveinting some simple errors * Treating error once we try to set hotkey definitions * Adding ui notification on setHotkeys errors * Implementing a service queue request to hold until functions are implemented * Making sure toFixed is only called on Numbers Co-authored-by: Danny Brown <danny.ri.brown@gmail.com> Co-authored-by: Gustavo André Lelis <galelis@gmail.com>
This commit is contained in:
parent
9a62c28b3f
commit
2f30e7a821
@ -1,4 +1,5 @@
|
|||||||
export default {
|
export default {
|
||||||
warn: jest.fn(),
|
warn: jest.fn(),
|
||||||
error: jest.fn(),
|
error: jest.fn(),
|
||||||
|
info: jest.fn(),
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import hotkeys from './hotkeys';
|
import hotkeys from './../utils/hotkeys';
|
||||||
import log from './../log.js';
|
import log from './../log.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -11,7 +11,7 @@ import log from './../log.js';
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
export class HotkeysManager {
|
export class HotkeysManager {
|
||||||
constructor(commandsManager) {
|
constructor(commandsManager, servicesManager) {
|
||||||
this.hotkeyDefinitions = {};
|
this.hotkeyDefinitions = {};
|
||||||
this.hotkeyDefaults = [];
|
this.hotkeyDefaults = [];
|
||||||
this.isEnabled = true;
|
this.isEnabled = true;
|
||||||
@ -22,9 +22,19 @@ export class HotkeysManager {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this._servicesManager = servicesManager;
|
||||||
this._commandsManager = commandsManager;
|
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
|
* Disables all hotkeys. Hotkeys added while disabled will not listen for
|
||||||
* input.
|
* input.
|
||||||
@ -45,38 +55,57 @@ export class HotkeysManager {
|
|||||||
/**
|
/**
|
||||||
* Registers a list of hotkeydefinitions.
|
* Registers a list of hotkeydefinitions.
|
||||||
*
|
*
|
||||||
* @param {HotkeyDefinition[] | Object} hotkeyDefinitions Contains hotkeys definitions
|
* @param {HotkeyDefinition[] | Object} [hotkeyDefinitions=[]] Contains hotkeys definitions
|
||||||
*/
|
*/
|
||||||
setHotkeys(hotkeyDefinitions) {
|
setHotkeys(hotkeyDefinitions = []) {
|
||||||
const definitions = Array.isArray(hotkeyDefinitions)
|
try {
|
||||||
? [...hotkeyDefinitions]
|
const definitions = this._getValidDefinitions(hotkeyDefinitions);
|
||||||
: this._parseToArrayLike(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
|
* Set default hotkey bindings. These
|
||||||
* values are used in `this.restoreDefaultBindings`.
|
* 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)
|
const definitions = Array.isArray(hotkeyDefinitions)
|
||||||
? [...hotkeyDefinitions]
|
? [...hotkeyDefinitions]
|
||||||
: this._parseToArrayLike(hotkeyDefinitions);
|
: this._parseToArrayLike(hotkeyDefinitions);
|
||||||
|
|
||||||
this.hotkeyDefaults = definitions;
|
return definitions;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* It parses given object containing hotkeyDefinition to array like.
|
* 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
|
* 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[]}
|
* @returns {HotkeyDefinition[]}
|
||||||
*/
|
*/
|
||||||
_parseToArrayLike(hotkeyDefinitionsObj) {
|
_parseToArrayLike(hotkeyDefinitionsObj = {}) {
|
||||||
const copy = { ...hotkeyDefinitionsObj };
|
const copy = { ...hotkeyDefinitionsObj };
|
||||||
return Object.entries(copy).map(entryValue =>
|
return Object.entries(copy).map(entryValue =>
|
||||||
this._parseToHotKeyObj(entryValue[0], entryValue[1])
|
this._parseToHotKeyObj(entryValue[0], entryValue[1])
|
||||||
@ -127,11 +156,13 @@ export class HotkeysManager {
|
|||||||
if (previouslyRegisteredDefinition) {
|
if (previouslyRegisteredDefinition) {
|
||||||
const previouslyRegisteredKeys = previouslyRegisteredDefinition.keys;
|
const previouslyRegisteredKeys = previouslyRegisteredDefinition.keys;
|
||||||
this._unbindHotkeys(commandName, previouslyRegisteredKeys);
|
this._unbindHotkeys(commandName, previouslyRegisteredKeys);
|
||||||
|
log.info(`Unbinding ${commandName} from ${previouslyRegisteredKeys}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set definition & bind
|
// Set definition & bind
|
||||||
this.hotkeyDefinitions[commandName] = { keys, label };
|
this.hotkeyDefinitions[commandName] = { keys, label };
|
||||||
this._bindHotkeys(commandName, keys);
|
this._bindHotkeys(commandName, keys);
|
||||||
|
log.info(`Binding ${commandName} to ${keys}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -167,12 +198,11 @@ export class HotkeysManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const isKeyArray = keys instanceof Array;
|
const isKeyArray = keys instanceof Array;
|
||||||
if (isKeyArray) {
|
const combinedKeys = isKeyArray ? keys.join('+') : keys;
|
||||||
keys.forEach(key => this._bindHotkeys(commandName, key));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
hotkeys.bind(keys, evt => {
|
hotkeys.bind(combinedKeys, evt => {
|
||||||
|
evt.preventDefault();
|
||||||
|
evt.stopPropagation();
|
||||||
this._commandsManager.runCommand(commandName, { evt });
|
this._commandsManager.runCommand(commandName, { evt });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -193,7 +223,8 @@ export class HotkeysManager {
|
|||||||
|
|
||||||
const isKeyArray = keys instanceof Array;
|
const isKeyArray = keys instanceof Array;
|
||||||
if (isKeyArray) {
|
if (isKeyArray) {
|
||||||
keys.forEach(key => this._unbindHotkeys(commandName, key));
|
const combinedKeys = keys.join('+');
|
||||||
|
this._unbindHotkeys(commandName, combinedKeys);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
import CommandsManager from './CommandsManager.js';
|
import CommandsManager from './CommandsManager.js';
|
||||||
import HotkeysManager from './HotkeysManager.js';
|
import HotkeysManager from './HotkeysManager.js';
|
||||||
import hotkeys from './hotkeys';
|
import hotkeys from './../utils/hotkeys';
|
||||||
import log from './../log.js';
|
import log from './../log.js';
|
||||||
|
|
||||||
jest.mock('./CommandsManager.js');
|
jest.mock('./CommandsManager.js');
|
||||||
jest.mock('./hotkeys');
|
jest.mock('./../utils/hotkeys');
|
||||||
jest.mock('./../log.js');
|
jest.mock('./../log.js');
|
||||||
|
|
||||||
describe('HotkeysManager', () => {
|
describe('HotkeysManager', () => {
|
||||||
@ -60,7 +60,10 @@ describe('HotkeysManager', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('enable()', () => {
|
describe('enable()', () => {
|
||||||
beforeEach(() => hotkeys.unpause.mockClear());
|
beforeEach(() => {
|
||||||
|
hotkeys.unpause = jest.fn();
|
||||||
|
hotkeys.unpause.mockClear();
|
||||||
|
});
|
||||||
|
|
||||||
it('sets isEnabled property to true', () => {
|
it('sets isEnabled property to true', () => {
|
||||||
hotkeysManager.disable();
|
hotkeysManager.disable();
|
||||||
@ -139,20 +142,18 @@ describe('HotkeysManager', () => {
|
|||||||
expectedHotkeyDefinition
|
expectedHotkeyDefinition
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
it('calls hotkeys.bind for all keys in array', () => {
|
it('calls hotkeys.bind for the group of keys', () => {
|
||||||
const definition = { commandName: 'dance', keys: ['h', 'e', 'l', 'o'] };
|
const definition = { commandName: 'dance', keys: ['shift', 'e'] };
|
||||||
|
|
||||||
hotkeysManager.registerHotkeys(definition);
|
hotkeysManager.registerHotkeys(definition);
|
||||||
|
|
||||||
expect(hotkeys.bind.mock.calls.length).toBe(definition.keys.length);
|
expect(hotkeys.bind.mock.calls.length).toBe(1);
|
||||||
definition.keys.forEach((key, i) =>
|
expect(hotkeys.bind.mock.calls[0][0]).toBe('shift+e');
|
||||||
expect(hotkeys.bind.mock.calls[i][0]).toBe(key)
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
it('calls hotkeys.unbind if commandName was previously registered, for each previously registered set of keys', () => {
|
it('calls hotkeys.unbind if commandName was previously registered, for each previously registered set of keys', () => {
|
||||||
const firstDefinition = {
|
const firstDefinition = {
|
||||||
commandName: 'dance',
|
commandName: 'dance',
|
||||||
keys: ['h', 'e', 'l', 'o'],
|
keys: ['alt', 'e'],
|
||||||
};
|
};
|
||||||
const secondDefinition = { commandName: 'dance', keys: 'a' };
|
const secondDefinition = { commandName: 'dance', keys: 'a' };
|
||||||
|
|
||||||
@ -161,12 +162,8 @@ describe('HotkeysManager', () => {
|
|||||||
// Second call
|
// Second call
|
||||||
hotkeysManager.registerHotkeys(secondDefinition);
|
hotkeysManager.registerHotkeys(secondDefinition);
|
||||||
|
|
||||||
expect(hotkeys.unbind.mock.calls.length).toBe(
|
expect(hotkeys.unbind.mock.calls.length).toBe(1);
|
||||||
firstDefinition.keys.length
|
expect(hotkeys.unbind.mock.calls[0][0]).toBe('alt+e');
|
||||||
);
|
|
||||||
firstDefinition.keys.forEach((key, i) =>
|
|
||||||
expect(hotkeys.unbind.mock.calls[i][0]).toBe(key)
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -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;
|
|
||||||
@ -18,7 +18,7 @@ import string from './string.js';
|
|||||||
import studies from './studies/';
|
import studies from './studies/';
|
||||||
import ui from './ui';
|
import ui from './ui';
|
||||||
import user from './user.js';
|
import user from './user.js';
|
||||||
import utils from './utils/';
|
import utils, { hotkeys } from './utils/';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
UINotificationService,
|
UINotificationService,
|
||||||
@ -36,6 +36,7 @@ const OHIF = {
|
|||||||
ServicesManager,
|
ServicesManager,
|
||||||
//
|
//
|
||||||
utils,
|
utils,
|
||||||
|
hotkeys,
|
||||||
studies,
|
studies,
|
||||||
redux,
|
redux,
|
||||||
classes,
|
classes,
|
||||||
@ -68,6 +69,7 @@ export {
|
|||||||
ServicesManager,
|
ServicesManager,
|
||||||
//
|
//
|
||||||
utils,
|
utils,
|
||||||
|
hotkeys,
|
||||||
studies,
|
studies,
|
||||||
redux,
|
redux,
|
||||||
classes,
|
classes,
|
||||||
|
|||||||
@ -16,6 +16,7 @@ describe('Top level exports', () => {
|
|||||||
'MeasurementService',
|
'MeasurementService',
|
||||||
//
|
//
|
||||||
'utils',
|
'utils',
|
||||||
|
'hotkeys',
|
||||||
'studies',
|
'studies',
|
||||||
'redux',
|
'redux',
|
||||||
'classes',
|
'classes',
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
const displayFunction = data => {
|
const displayFunction = data => {
|
||||||
let text = '';
|
let text = '';
|
||||||
if (data.rAngle) {
|
if (data.rAngle && !isNaN(data.rAngle)) {
|
||||||
text = data.rAngle.toFixed(2) + String.fromCharCode(parseInt('00B0', 16));
|
text = data.rAngle.toFixed(2) + String.fromCharCode(parseInt('00B0', 16));
|
||||||
}
|
}
|
||||||
return text;
|
return text;
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
const displayFunction = data => {
|
const displayFunction = data => {
|
||||||
let meanValue = '';
|
let meanValue = '';
|
||||||
const { cachedStats } = data;
|
const { cachedStats } = data;
|
||||||
if (cachedStats && cachedStats.mean) {
|
if (cachedStats && cachedStats.mean && !isNaN(cachedStats.mean)) {
|
||||||
meanValue = cachedStats.mean.toFixed(2) + ' HU';
|
meanValue = cachedStats.mean.toFixed(2) + ' HU';
|
||||||
}
|
}
|
||||||
return meanValue;
|
return meanValue;
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
const displayFunction = data => {
|
const displayFunction = data => {
|
||||||
let meanValue = '';
|
let meanValue = '';
|
||||||
const { cachedStats } = data;
|
const { cachedStats } = data;
|
||||||
if (cachedStats && cachedStats.mean) {
|
if (cachedStats && cachedStats.mean && !isNaN(cachedStats.mean)) {
|
||||||
meanValue = cachedStats.mean.toFixed(2) + ' HU';
|
meanValue = cachedStats.mean.toFixed(2) + ' HU';
|
||||||
}
|
}
|
||||||
return meanValue;
|
return meanValue;
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
const displayFunction = data => {
|
const displayFunction = data => {
|
||||||
let meanValue = '';
|
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';
|
meanValue = data.meanStdDev.mean.toFixed(2) + ' HU';
|
||||||
}
|
}
|
||||||
return meanValue;
|
return meanValue;
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
const displayFunction = data => {
|
const displayFunction = data => {
|
||||||
let lengthValue = '';
|
let lengthValue = '';
|
||||||
if (data.length) {
|
if (data.length && !isNaN(data.length)) {
|
||||||
lengthValue = data.length.toFixed(2) + ' mm';
|
lengthValue = data.length.toFixed(2) + ' mm';
|
||||||
}
|
}
|
||||||
return lengthValue;
|
return lengthValue;
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
const displayFunction = data => {
|
const displayFunction = data => {
|
||||||
let meanValue = '';
|
let meanValue = '';
|
||||||
const { cachedStats } = data;
|
const { cachedStats } = data;
|
||||||
if (cachedStats && cachedStats.mean) {
|
if (cachedStats && cachedStats.mean && !isNaN(cachedStats.mean)) {
|
||||||
meanValue = cachedStats.mean.toFixed(2) + ' HU';
|
meanValue = cachedStats.mean.toFixed(2) + ' HU';
|
||||||
}
|
}
|
||||||
return meanValue;
|
return meanValue;
|
||||||
|
|||||||
@ -1,12 +1,6 @@
|
|||||||
import cloneDeep from 'lodash.clonedeep';
|
import cloneDeep from 'lodash.clonedeep';
|
||||||
|
|
||||||
const defaultState = {
|
const defaultState = {
|
||||||
// First tab
|
|
||||||
hotkeyDefinitions: [
|
|
||||||
// commandName, label, keys
|
|
||||||
// [{ zoom: { label: 'Zoom', keys: ['z'] }}]
|
|
||||||
],
|
|
||||||
// Second tab
|
|
||||||
windowLevelData: {
|
windowLevelData: {
|
||||||
// order, description, window (int), level (int)
|
// order, description, window (int), level (int)
|
||||||
// 0: { description: 'Soft tissue', window: '', level: '' },
|
// 0: { description: 'Soft tissue', window: '', level: '' },
|
||||||
|
|||||||
@ -12,6 +12,8 @@
|
|||||||
|
|
||||||
const name = 'UINotificationService';
|
const name = 'UINotificationService';
|
||||||
|
|
||||||
|
const serviceShowRequestQueue = [];
|
||||||
|
|
||||||
const publicAPI = {
|
const publicAPI = {
|
||||||
name,
|
name,
|
||||||
hide: _hide,
|
hide: _hide,
|
||||||
@ -21,7 +23,11 @@ const publicAPI = {
|
|||||||
|
|
||||||
const serviceImplementation = {
|
const serviceImplementation = {
|
||||||
_hide: () => console.warn('hide() NOT IMPLEMENTED'),
|
_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) {
|
if (showImplementation) {
|
||||||
serviceImplementation._show = showImplementation;
|
serviceImplementation._show = showImplementation;
|
||||||
|
|
||||||
|
while (serviceShowRequestQueue.length > 0) {
|
||||||
|
const showArguments = serviceShowRequestQueue.pop();
|
||||||
|
serviceImplementation._show(showArguments);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
8
platform/core/src/utils/hotkeys/index.js
Normal file
8
platform/core/src/utils/hotkeys/index.js
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import Mousetrap from 'mousetrap';
|
||||||
|
import pausePlugin from './pausePlugin';
|
||||||
|
import recordPlugin from './recordPlugin';
|
||||||
|
|
||||||
|
recordPlugin(Mousetrap);
|
||||||
|
pausePlugin(Mousetrap);
|
||||||
|
|
||||||
|
export default Mousetrap;
|
||||||
@ -66,7 +66,7 @@ export default function(Mousetrap) {
|
|||||||
_recordCurrentCombo();
|
_recordCurrentCombo();
|
||||||
}
|
}
|
||||||
|
|
||||||
for (i = 0; i < modifiers.length; ++i) {
|
for (let i = 0; i < modifiers.length; ++i) {
|
||||||
_recordKey(modifiers[i]);
|
_recordKey(modifiers[i]);
|
||||||
}
|
}
|
||||||
_recordKey(character);
|
_recordKey(character);
|
||||||
@ -85,10 +85,8 @@ export default function(Mousetrap) {
|
|||||||
* @returns void
|
* @returns void
|
||||||
*/
|
*/
|
||||||
function _recordKey(key) {
|
function _recordKey(key) {
|
||||||
var i;
|
|
||||||
|
|
||||||
// one-off implementation of Array.indexOf, since IE6-9 don't support it
|
// 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) {
|
if (_currentRecordedKeys[i] === key) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -111,7 +109,7 @@ export default function(Mousetrap) {
|
|||||||
_recordedSequence.push(_currentRecordedKeys);
|
_recordedSequence.push(_currentRecordedKeys);
|
||||||
_currentRecordedKeys = [];
|
_currentRecordedKeys = [];
|
||||||
_recordedCharacterKey = false;
|
_recordedCharacterKey = false;
|
||||||
_restartRecordTimer();
|
_finishRecording();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -124,9 +122,7 @@ export default function(Mousetrap) {
|
|||||||
* @returns void
|
* @returns void
|
||||||
*/
|
*/
|
||||||
function _normalizeSequence(sequence) {
|
function _normalizeSequence(sequence) {
|
||||||
var i;
|
for (let i = 0; i < sequence.length; ++i) {
|
||||||
|
|
||||||
for (i = 0; i < sequence.length; ++i) {
|
|
||||||
sequence[i].sort(function(x, y) {
|
sequence[i].sort(function(x, y) {
|
||||||
// modifier keys always come first, in alphabetical order
|
// modifier keys always come first, in alphabetical order
|
||||||
if (x.length > 1 && y.length === 1) {
|
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() {
|
Mousetrap.prototype.handleKey = function() {
|
||||||
var self = this;
|
var self = this;
|
||||||
_handleKey.apply(self, arguments);
|
_handleKey.apply(self, arguments);
|
||||||
@ -12,6 +12,7 @@ import DicomLoaderService from './dicomLoaderService.js';
|
|||||||
import b64toBlob from './b64toBlob.js';
|
import b64toBlob from './b64toBlob.js';
|
||||||
import * as urlUtil from './urlUtil';
|
import * as urlUtil from './urlUtil';
|
||||||
import makeCancelable from './makeCancelable';
|
import makeCancelable from './makeCancelable';
|
||||||
|
import hotkeys from './hotkeys';
|
||||||
|
|
||||||
const utils = {
|
const utils = {
|
||||||
guid,
|
guid,
|
||||||
@ -29,6 +30,7 @@ const utils = {
|
|||||||
DicomLoaderService,
|
DicomLoaderService,
|
||||||
urlUtil,
|
urlUtil,
|
||||||
makeCancelable,
|
makeCancelable,
|
||||||
|
hotkeys,
|
||||||
};
|
};
|
||||||
|
|
||||||
export {
|
export {
|
||||||
@ -47,6 +49,7 @@ export {
|
|||||||
DicomLoaderService,
|
DicomLoaderService,
|
||||||
urlUtil,
|
urlUtil,
|
||||||
makeCancelable,
|
makeCancelable,
|
||||||
|
hotkeys,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default utils;
|
export default utils;
|
||||||
|
|||||||
@ -18,6 +18,7 @@ describe('Top level exports', () => {
|
|||||||
'DicomLoaderService',
|
'DicomLoaderService',
|
||||||
'urlUtil',
|
'urlUtil',
|
||||||
'makeCancelable',
|
'makeCancelable',
|
||||||
|
'hotkeys',
|
||||||
].sort();
|
].sort();
|
||||||
|
|
||||||
const exports = Object.keys(utils.default).sort();
|
const exports = Object.keys(utils.default).sort();
|
||||||
|
|||||||
70
platform/i18n/src/getAvailableLanguagesInfo.js
Normal file
70
platform/i18n/src/getAvailableLanguagesInfo.js
Normal file
@ -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;
|
||||||
|
}
|
||||||
@ -140,4 +140,7 @@ i18n.initI18n = initI18n;
|
|||||||
i18n.addLocales = addLocales;
|
i18n.addLocales = addLocales;
|
||||||
i18n.defaultLanguage = DEFAULT_LANGUAGE;
|
i18n.defaultLanguage = DEFAULT_LANGUAGE;
|
||||||
|
|
||||||
|
import getAvailableLanguagesInfo from './getAvailableLanguagesInfo.js';
|
||||||
|
i18n.availableLanguages = getAvailableLanguagesInfo(locales);
|
||||||
|
|
||||||
export default i18n;
|
export default i18n;
|
||||||
|
|||||||
82
platform/ui/src/components/customForm/HotkeyField.js
Normal file
82
platform/ui/src/components/customForm/HotkeyField.js
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import { hotkeys } from '@ohif/core';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Take the pressed key array and return the readable string for the keys
|
||||||
|
*
|
||||||
|
* @param {Array} [keys=[]]
|
||||||
|
* @returns {string} string representation of an array of keys
|
||||||
|
*/
|
||||||
|
const formatKeysForInput = (keys = []) => keys.join('+');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* formats given keys sequence to insert the modifier keys in the first index of the array
|
||||||
|
* @param {string} sequence keys sequence from MouseTrap Record -> "shift+left"
|
||||||
|
* @returns {Array} keys in array-format -> ['shift','left']
|
||||||
|
*/
|
||||||
|
const getKeys = ({ sequence, modifier_keys }) => {
|
||||||
|
const keysArray = sequence.join(' ').split('+');
|
||||||
|
let keys = [];
|
||||||
|
let modifiers = [];
|
||||||
|
keysArray.forEach(key => {
|
||||||
|
if (modifier_keys && modifier_keys.includes(key)) {
|
||||||
|
modifiers.push(key);
|
||||||
|
} else {
|
||||||
|
keys.push(key);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return [...modifiers, ...keys];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HotkeyField
|
||||||
|
* Renders a hotkey input
|
||||||
|
*
|
||||||
|
* @param {object} props component props
|
||||||
|
* @param {Array[]} props.keys array of keys to be controlled by this field
|
||||||
|
* @param {function} props.handleChange Callback function to communicate parent once value is changed
|
||||||
|
* @param {string} props.classNames string caontaining classes to be added in the input field
|
||||||
|
* @param {Array[]} props.modifier_keys
|
||||||
|
*/
|
||||||
|
function HotkeyField({ keys, handleChange, classNames, modifier_keys }) {
|
||||||
|
const inputValue = formatKeysForInput(keys);
|
||||||
|
|
||||||
|
const onInputKeyDown = event => {
|
||||||
|
event.stopPropagation();
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
hotkeys.record(sequence => {
|
||||||
|
const keys = getKeys({ sequence, modifier_keys });
|
||||||
|
hotkeys.unpause();
|
||||||
|
handleChange(keys);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onFocus = () => {
|
||||||
|
hotkeys.pause();
|
||||||
|
hotkeys.startRecording();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
readOnly={true}
|
||||||
|
type="text"
|
||||||
|
value={inputValue}
|
||||||
|
className={classNames}
|
||||||
|
onKeyDown={onInputKeyDown}
|
||||||
|
onFocus={onFocus}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
HotkeyField.propTypes = {
|
||||||
|
keys: PropTypes.array.isRequired,
|
||||||
|
handleChange: PropTypes.func.isRequired,
|
||||||
|
classNames: PropTypes.string,
|
||||||
|
modifier_keys: PropTypes.array,
|
||||||
|
allowed_keys: PropTypes.array,
|
||||||
|
};
|
||||||
|
|
||||||
|
export { HotkeyField };
|
||||||
1
platform/ui/src/components/customForm/index.js
Normal file
1
platform/ui/src/components/customForm/index.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { HotkeyField } from './HotkeyField';
|
||||||
@ -3,11 +3,10 @@ import { LayoutButton, LayoutChooser } from './layoutButton';
|
|||||||
import { MeasurementTable, MeasurementTableItem } from './measurementTable';
|
import { MeasurementTable, MeasurementTableItem } from './measurementTable';
|
||||||
import { Overlay, OverlayTrigger } from './overlayTrigger';
|
import { Overlay, OverlayTrigger } from './overlayTrigger';
|
||||||
import { TableList, TableListItem } from './tableList';
|
import { TableList, TableListItem } from './tableList';
|
||||||
import {
|
import { AboutContent } from './content/aboutContent/AboutContent';
|
||||||
AboutContent,
|
import { TabComponents, TabFooter } from './tabComponents';
|
||||||
UserPreferences,
|
import { HotkeyField } from './customForm';
|
||||||
UserPreferencesForm,
|
import { LanguageSwitcher } from './languageSwitcher';
|
||||||
} from './userPreferencesForm';
|
|
||||||
|
|
||||||
import { Checkbox } from './checkbox';
|
import { Checkbox } from './checkbox';
|
||||||
import { CineDialog } from './cineDialog';
|
import { CineDialog } from './cineDialog';
|
||||||
@ -48,12 +47,14 @@ export {
|
|||||||
TableList,
|
TableList,
|
||||||
TableListItem,
|
TableListItem,
|
||||||
Thumbnail,
|
Thumbnail,
|
||||||
|
TabComponents,
|
||||||
|
TabFooter,
|
||||||
|
HotkeyField,
|
||||||
|
LanguageSwitcher,
|
||||||
TableSearchFilter,
|
TableSearchFilter,
|
||||||
TablePagination,
|
TablePagination,
|
||||||
ToolbarSection,
|
ToolbarSection,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
AboutContent,
|
AboutContent,
|
||||||
UserPreferences,
|
|
||||||
UserPreferencesForm,
|
|
||||||
OHIFModal,
|
OHIFModal,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -2,26 +2,11 @@ import React from 'react';
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import './LanguageSwitcher.styl';
|
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 onChange = event => {
|
||||||
const { value } = event.target;
|
const { value } = event.target;
|
||||||
onLanguageChange(parseLanguage(value));
|
onLanguageChange(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -29,7 +14,7 @@ const LanguageSwitcher = ({ language, onLanguageChange }) => {
|
|||||||
name="language-select"
|
name="language-select"
|
||||||
id="language-select"
|
id="language-select"
|
||||||
className="language-select"
|
className="language-select"
|
||||||
value={parseLanguage(language)}
|
value={language}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
>
|
>
|
||||||
{languages.map(lng => (
|
{languages.map(lng => (
|
||||||
@ -43,7 +28,8 @@ const LanguageSwitcher = ({ language, onLanguageChange }) => {
|
|||||||
|
|
||||||
LanguageSwitcher.propTypes = {
|
LanguageSwitcher.propTypes = {
|
||||||
language: PropTypes.string.isRequired,
|
language: PropTypes.string.isRequired,
|
||||||
|
languages: PropTypes.array.isRequired,
|
||||||
onLanguageChange: PropTypes.func.isRequired,
|
onLanguageChange: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default withTranslation('UserPreferencesModal')(LanguageSwitcher);
|
export { LanguageSwitcher };
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
export { default } from './LanguageSwitcher';
|
export { LanguageSwitcher } from './LanguageSwitcher';
|
||||||
|
|||||||
105
platform/ui/src/components/tabComponents/TabComponents.js
Normal file
105
platform/ui/src/components/tabComponents/TabComponents.js
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import classnames from 'classnames';
|
||||||
|
|
||||||
|
// Style
|
||||||
|
import './TabComponents.styl';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Take name of the tab and create the data-cy value for it
|
||||||
|
*
|
||||||
|
* @param {string} [name=''] tab name
|
||||||
|
* @returns {string} data-cy value
|
||||||
|
*/
|
||||||
|
const getDataCy = (name = '') => {
|
||||||
|
return name
|
||||||
|
.split(' ')
|
||||||
|
.join('-')
|
||||||
|
.toLowerCase();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Single tab data information
|
||||||
|
*
|
||||||
|
* @typedef {Object} tabData
|
||||||
|
* @property {string} name - name of the tab
|
||||||
|
* @property {Object} Component - tab component to be rendered
|
||||||
|
* @property {Object} customProps - tab custom properties
|
||||||
|
* @property {bool} hidden - bool to define if tab is hidden of not
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Take a list of components data and render then into tabs
|
||||||
|
*
|
||||||
|
* @param {Object} props
|
||||||
|
* @param {[tabData]} props.tabs array of tab data
|
||||||
|
* @param {Object} props.customProps common custom properties
|
||||||
|
*/
|
||||||
|
function TabComponents({ tabs, customProps = {} }) {
|
||||||
|
const [currentTabIndex, setCurrentTabIndex] = useState(0);
|
||||||
|
|
||||||
|
return (
|
||||||
|
tabs.length > 0 && (
|
||||||
|
<div className="TabComponents">
|
||||||
|
<div className="TabComponents_tabHeader">
|
||||||
|
<div className="TabComponents_tabHeader_selector">
|
||||||
|
<div className="dialog-separator-after">
|
||||||
|
<ul className="nav nav-tabs">
|
||||||
|
{tabs.map((tab, index) => {
|
||||||
|
const { name, hidden } = tab;
|
||||||
|
return (
|
||||||
|
!hidden && (
|
||||||
|
<li
|
||||||
|
key={index}
|
||||||
|
onClick={() => {
|
||||||
|
setCurrentTabIndex(index);
|
||||||
|
}}
|
||||||
|
className={classnames(
|
||||||
|
'nav-link',
|
||||||
|
index === currentTabIndex && 'active'
|
||||||
|
)}
|
||||||
|
data-cy={getDataCy(name)}
|
||||||
|
>
|
||||||
|
<button>{name}</button>
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{tabs.map((tab, index) => {
|
||||||
|
const { Component, customProps: tabCustomProps, hidden } = tab;
|
||||||
|
return (
|
||||||
|
!hidden && (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className={classnames(
|
||||||
|
'TabComponents_content',
|
||||||
|
index === currentTabIndex && 'active'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Component {...customProps} {...tabCustomProps} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
TabComponents.propTypes = {
|
||||||
|
tabs: PropTypes.arrayOf(
|
||||||
|
PropTypes.shape({
|
||||||
|
name: PropTypes.string,
|
||||||
|
Component: PropTypes.any,
|
||||||
|
customProps: PropTypes.object,
|
||||||
|
hidden: PropTypes.bool,
|
||||||
|
})
|
||||||
|
),
|
||||||
|
customProps: PropTypes.object,
|
||||||
|
};
|
||||||
|
|
||||||
|
export { TabComponents };
|
||||||
21
platform/ui/src/components/tabComponents/TabComponents.styl
Normal file
21
platform/ui/src/components/tabComponents/TabComponents.styl
Normal file
@ -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
|
||||||
54
platform/ui/src/components/tabComponents/TabFooter.js
Normal file
54
platform/ui/src/components/tabComponents/TabFooter.js
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import './TabFooter.styl';
|
||||||
|
|
||||||
|
// In case translate is not passed
|
||||||
|
const translate = word => word;
|
||||||
|
|
||||||
|
function TabFooter({
|
||||||
|
onResetPreferences,
|
||||||
|
onSave,
|
||||||
|
onCancel,
|
||||||
|
hasErrors,
|
||||||
|
t = translate,
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="footer">
|
||||||
|
<button
|
||||||
|
className="btn btn-danger pull-left"
|
||||||
|
data-cy="reset-default-btn"
|
||||||
|
onClick={onResetPreferences}
|
||||||
|
>
|
||||||
|
{t('Reset to Defaults')}
|
||||||
|
</button>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
onClick={onCancel}
|
||||||
|
data-cy="cancel-btn"
|
||||||
|
className="btn btn-default"
|
||||||
|
>
|
||||||
|
{t('Cancel')}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
className="btn btn-primary"
|
||||||
|
data-cy="save-btn"
|
||||||
|
disabled={hasErrors}
|
||||||
|
onClick={onSave}
|
||||||
|
>
|
||||||
|
{t('Save')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
TabFooter.propTypes = {
|
||||||
|
onResetPreferences: PropTypes.func,
|
||||||
|
onSave: PropTypes.func,
|
||||||
|
onCancel: PropTypes.func,
|
||||||
|
hasErrors: PropTypes.bool,
|
||||||
|
t: PropTypes.func,
|
||||||
|
};
|
||||||
|
|
||||||
|
export { TabFooter };
|
||||||
11
platform/ui/src/components/tabComponents/TabFooter.styl
Normal file
11
platform/ui/src/components/tabComponents/TabFooter.styl
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
.footer
|
||||||
|
display: flex
|
||||||
|
flex-direction: row
|
||||||
|
justify-content: space-between
|
||||||
|
padding: 20px 20px 0 20px
|
||||||
|
margin: 0 -20px
|
||||||
|
border-top: 3px solid var(--primary-background-color)
|
||||||
|
|
||||||
|
div
|
||||||
|
button:last-child
|
||||||
|
margin-left: 10px
|
||||||
@ -0,0 +1,27 @@
|
|||||||
|
---
|
||||||
|
name: Tab Components
|
||||||
|
menu: Components
|
||||||
|
route: /components/tab-components
|
||||||
|
---
|
||||||
|
|
||||||
|
import { Playground, Props } from 'docz'
|
||||||
|
import { State } from 'react-powerplug'
|
||||||
|
import { TabComponents } from './../index.js'
|
||||||
|
|
||||||
|
// Data
|
||||||
|
import tabs from './tabs.js'
|
||||||
|
|
||||||
|
# Tab Components
|
||||||
|
|
||||||
|
## Basic usage
|
||||||
|
|
||||||
|
<Playground>
|
||||||
|
<TabComponents
|
||||||
|
tabs={tabs}
|
||||||
|
customProps={}
|
||||||
|
/>
|
||||||
|
</Playground>
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
<Props of={TabComponents} />
|
||||||
26
platform/ui/src/components/tabComponents/__docs__/tabs.js
Normal file
26
platform/ui/src/components/tabComponents/__docs__/tabs.js
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
export default tabs = [
|
||||||
|
{
|
||||||
|
name: 'tabName1',
|
||||||
|
Component: () => {
|
||||||
|
return <div>tab 1 Content</div>;
|
||||||
|
},
|
||||||
|
customProps: {},
|
||||||
|
hidden: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'tabName2',
|
||||||
|
Component: () => {
|
||||||
|
return <div>tab 2 Content</div>;
|
||||||
|
},
|
||||||
|
customProps: {},
|
||||||
|
hidden: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'tabName3',
|
||||||
|
Component: () => {
|
||||||
|
return <div>tab 3 Content</div>;
|
||||||
|
},
|
||||||
|
customProps: {},
|
||||||
|
hidden: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
2
platform/ui/src/components/tabComponents/index.js
Normal file
2
platform/ui/src/components/tabComponents/index.js
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export { TabComponents } from './TabComponents';
|
||||||
|
export { TabFooter } from './TabFooter';
|
||||||
@ -1,57 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import LanguageSwitcher from '../languageSwitcher';
|
|
||||||
import i18n from '@ohif/i18n';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* General Preferences tab
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* General Preferences tab
|
|
||||||
* It renders the General Preferences content
|
|
||||||
*
|
|
||||||
* It stores current state and whenever it changes, component messages parent of new value (through function callback)
|
|
||||||
* @param {object} props component props
|
|
||||||
* @param {string} props.name Tab`s name
|
|
||||||
* @param {object} props.generalPreferences Data for initial state
|
|
||||||
* @param {function} props.onTabStateChanged Callback function to communicate parent in case its states changes
|
|
||||||
* @param {function} props.onTabErrorChanged Callback Function in case any error on tab
|
|
||||||
*/
|
|
||||||
function GeneralPreferences({
|
|
||||||
generalPreferences,
|
|
||||||
name,
|
|
||||||
onTabStateChanged,
|
|
||||||
onTabErrorChanged,
|
|
||||||
}) {
|
|
||||||
const { language = i18n.language } = generalPreferences;
|
|
||||||
|
|
||||||
const onLanguageChange = language => {
|
|
||||||
onTabStateChanged(name, {
|
|
||||||
generalPreferences: { ...generalPreferences, language },
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="general-preferences-wrapper">
|
|
||||||
<div className="col-sm-3">
|
|
||||||
<label htmlFor="language-select" className="p-r-1">
|
|
||||||
Language
|
|
||||||
</label>
|
|
||||||
<LanguageSwitcher
|
|
||||||
language={language}
|
|
||||||
onLanguageChange={onLanguageChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
GeneralPreferences.propTypes = {
|
|
||||||
generalPreferences: PropTypes.any,
|
|
||||||
name: PropTypes.string,
|
|
||||||
onTabStateChanged: PropTypes.func,
|
|
||||||
onTabErrorChanged: PropTypes.func,
|
|
||||||
};
|
|
||||||
|
|
||||||
export { GeneralPreferences };
|
|
||||||
@ -1,452 +0,0 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
|
||||||
import React, { useState, useEffect } from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import './HotKeysPreferences.styl';
|
|
||||||
|
|
||||||
import {
|
|
||||||
allowedKeys,
|
|
||||||
disallowedCombinations,
|
|
||||||
specialKeys,
|
|
||||||
} from './hotKeysConfig.js';
|
|
||||||
|
|
||||||
import isEqual from 'lodash.isequal';
|
|
||||||
|
|
||||||
const getKeysPressedArray = keyDownEvent => {
|
|
||||||
const keysPressedArray = [];
|
|
||||||
const { ctrlKey, altKey, shiftKey } = keyDownEvent;
|
|
||||||
|
|
||||||
if (ctrlKey && !altKey) {
|
|
||||||
keysPressedArray.push('ctrl');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (shiftKey && !altKey) {
|
|
||||||
keysPressedArray.push('shift');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (altKey && !ctrlKey) {
|
|
||||||
keysPressedArray.push('alt');
|
|
||||||
}
|
|
||||||
|
|
||||||
return keysPressedArray;
|
|
||||||
};
|
|
||||||
|
|
||||||
const findConflictingCommand = (
|
|
||||||
originalHotKeys,
|
|
||||||
currentCommandName,
|
|
||||||
currentHotKeys
|
|
||||||
) => {
|
|
||||||
let firstConflictingCommand = undefined;
|
|
||||||
|
|
||||||
for (const commandName in originalHotKeys) {
|
|
||||||
const toolHotKeys = originalHotKeys[commandName].keys;
|
|
||||||
|
|
||||||
if (
|
|
||||||
isEqual(toolHotKeys, currentHotKeys) &&
|
|
||||||
commandName !== currentCommandName
|
|
||||||
) {
|
|
||||||
firstConflictingCommand = originalHotKeys[commandName];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return firstConflictingCommand;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Splits given keysObj into arrays. Each array item will be a representation of column
|
|
||||||
* @param {obj} keysObj objects to be splitted into columns
|
|
||||||
* @param {number} columnSize How many rows per column
|
|
||||||
*/
|
|
||||||
const getHotKeysArrayColumns = (keysObj = {}, columnSize) => {
|
|
||||||
if (isNaN(columnSize)) {
|
|
||||||
return keysObj;
|
|
||||||
}
|
|
||||||
|
|
||||||
const keys = Object.keys(keysObj);
|
|
||||||
const keysValues = Object.values(keysObj);
|
|
||||||
const keysLength = keys.length;
|
|
||||||
|
|
||||||
// Columns from left should be bigger;
|
|
||||||
let currentColumn = 0;
|
|
||||||
const dividedKeys = [];
|
|
||||||
|
|
||||||
for (
|
|
||||||
let it = 0;
|
|
||||||
it < keysLength;
|
|
||||||
it++, it % columnSize === 0 ? currentColumn++ : currentColumn
|
|
||||||
) {
|
|
||||||
if (!dividedKeys[currentColumn]) {
|
|
||||||
dividedKeys[currentColumn] = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
dividedKeys[currentColumn][keys[it]] = keysValues[it];
|
|
||||||
}
|
|
||||||
|
|
||||||
return dividedKeys;
|
|
||||||
};
|
|
||||||
|
|
||||||
const NO_FIELD_ERROR_MESSAGE = undefined;
|
|
||||||
const formatPressedKeys = pressedKeysArray => pressedKeysArray.join('+');
|
|
||||||
const unFormatPressedKeys = (pressedKeysStr = '') => pressedKeysStr.split('+');
|
|
||||||
const inputValidators = (
|
|
||||||
commandName,
|
|
||||||
inputValue,
|
|
||||||
pressedKeys,
|
|
||||||
lastPressedKey,
|
|
||||||
originalHotKeys
|
|
||||||
) => {
|
|
||||||
let hasError = false;
|
|
||||||
let errorMessage = NO_FIELD_ERROR_MESSAGE;
|
|
||||||
|
|
||||||
const modifierValidator = ({ lastPressedKey }) => {
|
|
||||||
// Check if it has a valid modifier
|
|
||||||
const isModifier = ['ctrl', 'alt', 'shift'].includes(lastPressedKey);
|
|
||||||
if (isModifier) {
|
|
||||||
hasError = true;
|
|
||||||
errorMessage =
|
|
||||||
"It's not possible to define only modifier keys (ctrl, alt and shift) as a shortcut";
|
|
||||||
return {
|
|
||||||
hasError,
|
|
||||||
errorMessage,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const emptyValidator = ({ inputValue }) => {
|
|
||||||
if (!inputValue) {
|
|
||||||
hasError = true;
|
|
||||||
errorMessage = "Field can't be empty.";
|
|
||||||
return {
|
|
||||||
hasError,
|
|
||||||
errorMessage,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const conflictingValidator = ({
|
|
||||||
commandName,
|
|
||||||
pressedKeys,
|
|
||||||
originalHotKeys,
|
|
||||||
}) => {
|
|
||||||
const conflictingCommand = findConflictingCommand(
|
|
||||||
originalHotKeys,
|
|
||||||
commandName,
|
|
||||||
pressedKeys
|
|
||||||
);
|
|
||||||
if (conflictingCommand) {
|
|
||||||
hasError = true;
|
|
||||||
errorMessage = `"${conflictingCommand.label}" is already using the "${pressedKeys}" shortcut.`;
|
|
||||||
return {
|
|
||||||
hasError,
|
|
||||||
errorMessage,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const disallowedValidator = ({ inputValue, pressedKeys, lastPressedKey }) => {
|
|
||||||
const modifierCommand = formatPressedKeys(
|
|
||||||
pressedKeys.slice(0, pressedKeys.length - 1)
|
|
||||||
);
|
|
||||||
|
|
||||||
const disallowedCombination = disallowedCombinations[modifierCommand];
|
|
||||||
const hasDisallowedCombinations = disallowedCombination
|
|
||||||
? disallowedCombination.includes(lastPressedKey)
|
|
||||||
: false;
|
|
||||||
|
|
||||||
if (hasDisallowedCombinations) {
|
|
||||||
hasError = true;
|
|
||||||
errorMessage = `"${inputValue}" shortcut combination is not allowed`;
|
|
||||||
return {
|
|
||||||
hasError,
|
|
||||||
errorMessage,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const validators = [
|
|
||||||
emptyValidator,
|
|
||||||
modifierValidator,
|
|
||||||
conflictingValidator,
|
|
||||||
disallowedValidator,
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const validator of validators) {
|
|
||||||
const validation = validator({
|
|
||||||
commandName,
|
|
||||||
inputValue,
|
|
||||||
pressedKeys,
|
|
||||||
lastPressedKey,
|
|
||||||
originalHotKeys,
|
|
||||||
});
|
|
||||||
if (validation && validation.hasError) {
|
|
||||||
return validation;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// validation has passed successfully
|
|
||||||
return {
|
|
||||||
hasError,
|
|
||||||
errorMessage,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* HotKeysPreferencesRow
|
|
||||||
* Renders row for hotkey preference
|
|
||||||
* It stores current state and whenever it changes, component messages parent of new value (through function callback)
|
|
||||||
* @param {object} props component props
|
|
||||||
* @param {string} props.commandName command name associated to given row
|
|
||||||
* @param {string[]} props.hotkeys keys associated to given command
|
|
||||||
* @param {object} props.originalHotKeys original hotkeys values
|
|
||||||
* @param {function} props.onSuccessChanged Callback function to communicate parent in case its states changes
|
|
||||||
* @param {function} props.onFailureChanged Callback Function in case any error on row
|
|
||||||
*/
|
|
||||||
function HotKeyPreferencesRow({
|
|
||||||
commandName,
|
|
||||||
hotkeys,
|
|
||||||
label,
|
|
||||||
originalHotKeys,
|
|
||||||
tabError,
|
|
||||||
onSuccessChanged,
|
|
||||||
onFailureChanged,
|
|
||||||
}) {
|
|
||||||
const [inputValue, setInputValue] = useState(formatPressedKeys(hotkeys));
|
|
||||||
const [error, setError] = useState(false);
|
|
||||||
|
|
||||||
const [fieldErrorMessage, setFieldErrorMessage] = useState(
|
|
||||||
NO_FIELD_ERROR_MESSAGE
|
|
||||||
);
|
|
||||||
|
|
||||||
// reset error count if tab has no errors
|
|
||||||
useEffect(() => {
|
|
||||||
if (!tabError) {
|
|
||||||
setError(false);
|
|
||||||
setFieldErrorMessage(NO_FIELD_ERROR_MESSAGE);
|
|
||||||
setInputValue(formatPressedKeys(hotkeys));
|
|
||||||
}
|
|
||||||
}, [tabError]);
|
|
||||||
|
|
||||||
// update state values if props changes
|
|
||||||
useEffect(() => {
|
|
||||||
setInputValue(formatPressedKeys(hotkeys));
|
|
||||||
}, [hotkeys]);
|
|
||||||
|
|
||||||
const updateInputText = (keyDownEvent, displayPressedKey = false) => {
|
|
||||||
const pressedKeys = getKeysPressedArray(keyDownEvent);
|
|
||||||
|
|
||||||
if (displayPressedKey) {
|
|
||||||
const specialKeyName = specialKeys[keyDownEvent.which];
|
|
||||||
const keyName =
|
|
||||||
specialKeyName ||
|
|
||||||
keyDownEvent.key ||
|
|
||||||
String.fromCharCode(keyDownEvent.keyCode);
|
|
||||||
|
|
||||||
// ensure lowerCase
|
|
||||||
pressedKeys.push(keyName.toLowerCase());
|
|
||||||
}
|
|
||||||
|
|
||||||
setInputValue(formatPressedKeys(pressedKeys));
|
|
||||||
};
|
|
||||||
|
|
||||||
// validate input value
|
|
||||||
const validateInput = () => {
|
|
||||||
const pressedKeys = unFormatPressedKeys(inputValue);
|
|
||||||
const lastPressedKey = pressedKeys[pressedKeys.length - 1];
|
|
||||||
|
|
||||||
const {
|
|
||||||
hasError = false,
|
|
||||||
errorMessage = NO_FIELD_ERROR_MESSAGE,
|
|
||||||
} = inputValidators(
|
|
||||||
commandName,
|
|
||||||
inputValue,
|
|
||||||
pressedKeys,
|
|
||||||
lastPressedKey,
|
|
||||||
originalHotKeys
|
|
||||||
);
|
|
||||||
|
|
||||||
if (hasError) {
|
|
||||||
setInputValue('');
|
|
||||||
} else {
|
|
||||||
onSuccessChanged([inputValue]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasError !== error) {
|
|
||||||
setError(hasError);
|
|
||||||
}
|
|
||||||
|
|
||||||
setFieldErrorMessage(errorMessage);
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
onFailureChanged(error);
|
|
||||||
}, [error]);
|
|
||||||
|
|
||||||
const onInputKeyDown = event => {
|
|
||||||
// Prevent ESC key from propagating and closing the modal
|
|
||||||
if (event.key === 'Escape') {
|
|
||||||
event.stopPropagation();
|
|
||||||
}
|
|
||||||
|
|
||||||
updateInputText(event, allowedKeys.includes(event.keyCode));
|
|
||||||
event.preventDefault();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<tr key={commandName}>
|
|
||||||
<td className="text-right p-r-1">{label}</td>
|
|
||||||
<td width="200">
|
|
||||||
<label
|
|
||||||
className={`wrapperLabel ${
|
|
||||||
fieldErrorMessage !== undefined ? 'state-error' : ''
|
|
||||||
} `}
|
|
||||||
data-key="defaultTool"
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
readOnly={true}
|
|
||||||
type="text"
|
|
||||||
value={inputValue}
|
|
||||||
className="form-control hotkey text-center"
|
|
||||||
onKeyDown={onInputKeyDown}
|
|
||||||
onBlur={validateInput}
|
|
||||||
/>
|
|
||||||
<span className="wrapperText" />
|
|
||||||
<span className="errorMessage">{fieldErrorMessage}</span>
|
|
||||||
</label>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
HotKeyPreferencesRow.propTypes = {
|
|
||||||
commandName: PropTypes.string.isRequired,
|
|
||||||
hotkeys: PropTypes.array.isRequired,
|
|
||||||
label: PropTypes.string.isRequired,
|
|
||||||
originalHotKeys: PropTypes.object.isRequired,
|
|
||||||
tabError: PropTypes.bool.isRequired,
|
|
||||||
onSuccessChanged: PropTypes.func.isRequired,
|
|
||||||
onFailureChanged: PropTypes.func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* HotKeysPreferences tab
|
|
||||||
* It renders all hotkeys displayed into columns/rows
|
|
||||||
*
|
|
||||||
* It stores current state and whenever it changes, component messages parent of new value (through function callback)
|
|
||||||
* @param {object} props component props
|
|
||||||
* @param {string} props.name Tab`s name
|
|
||||||
* @param {object} props.hotkeyDefinitions Data for initial state
|
|
||||||
* @param {function} props.onTabStateChanged Callback function to communicate parent in case its states changes
|
|
||||||
* @param {function} props.onTabErrorChanged Callback Function in case any error on tab
|
|
||||||
*/
|
|
||||||
function HotKeysPreferences({
|
|
||||||
hotkeyDefinitions,
|
|
||||||
name,
|
|
||||||
tabError,
|
|
||||||
onTabStateChanged,
|
|
||||||
onTabErrorChanged,
|
|
||||||
}) {
|
|
||||||
const [tabState, setTabState] = useState(hotkeyDefinitions);
|
|
||||||
const [tabErrorCounter, setTabErrorCounter] = useState(0);
|
|
||||||
|
|
||||||
const [numColumns] = useState(2);
|
|
||||||
const [columnSize] = useState(() =>
|
|
||||||
Math.ceil(Object.keys(tabState || {}).length / numColumns)
|
|
||||||
);
|
|
||||||
|
|
||||||
const splittedHotKeys = getHotKeysArrayColumns(tabState, columnSize);
|
|
||||||
|
|
||||||
const onHotKeyChanged = (commandName, hotkeyDefinition, keys) => {
|
|
||||||
const newState = {
|
|
||||||
...tabState,
|
|
||||||
[commandName]: { ...hotkeyDefinition, keys },
|
|
||||||
};
|
|
||||||
setTabState(newState);
|
|
||||||
onTabStateChanged(name, { hotkeyDefinitions: newState });
|
|
||||||
};
|
|
||||||
|
|
||||||
const onErrorChanged = (toInc = true) => {
|
|
||||||
const increment = toInc ? 1 : -1;
|
|
||||||
const newValue = tabErrorCounter + increment;
|
|
||||||
if (newValue >= 0) {
|
|
||||||
setTabErrorCounter(newValue);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// reset error count if tab has no errors
|
|
||||||
useEffect(() => {
|
|
||||||
if (!tabError) {
|
|
||||||
setTabErrorCounter(0);
|
|
||||||
// update tab state
|
|
||||||
setTabState({ ...hotkeyDefinitions });
|
|
||||||
}
|
|
||||||
}, [tabError]);
|
|
||||||
|
|
||||||
// tell parent to update its state
|
|
||||||
useEffect(() => {
|
|
||||||
if (tabErrorCounter === 0) {
|
|
||||||
onTabErrorChanged(name, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tabErrorCounter === 1) {
|
|
||||||
onTabErrorChanged(name, true);
|
|
||||||
}
|
|
||||||
}, [tabErrorCounter]);
|
|
||||||
|
|
||||||
// update local state if parent updates
|
|
||||||
useEffect(() => {
|
|
||||||
setTabState({ ...hotkeyDefinitions });
|
|
||||||
}, [hotkeyDefinitions]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="HotKeysPreferences">
|
|
||||||
{splittedHotKeys.length > 0
|
|
||||||
? splittedHotKeys.map((columnHotKeys, index) => {
|
|
||||||
return (
|
|
||||||
<div className="column" key={index}>
|
|
||||||
<table className="full-width">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th className="text-right p-r-1">Function</th>
|
|
||||||
<th className="text-center">Shortcut</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{Object.entries(columnHotKeys).map(
|
|
||||||
hotkeyDefinitionTuple => (
|
|
||||||
<HotKeyPreferencesRow
|
|
||||||
key={hotkeyDefinitionTuple[0]}
|
|
||||||
commandName={hotkeyDefinitionTuple[0]}
|
|
||||||
hotkeys={hotkeyDefinitionTuple[1].keys}
|
|
||||||
label={hotkeyDefinitionTuple[1].label}
|
|
||||||
originalHotKeys={tabState}
|
|
||||||
tabError={tabError}
|
|
||||||
onSuccessChanged={keys =>
|
|
||||||
onHotKeyChanged(
|
|
||||||
hotkeyDefinitionTuple[0],
|
|
||||||
hotkeyDefinitionTuple[1],
|
|
||||||
keys
|
|
||||||
)
|
|
||||||
}
|
|
||||||
onFailureChanged={onErrorChanged}
|
|
||||||
></HotKeyPreferencesRow>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
: null}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
HotKeysPreferences.propTypes = {
|
|
||||||
hotkeyDefinitions: PropTypes.any,
|
|
||||||
name: PropTypes.string,
|
|
||||||
tabError: PropTypes.bool,
|
|
||||||
onTabStateChanged: PropTypes.func,
|
|
||||||
onTabErrorChanged: PropTypes.func,
|
|
||||||
};
|
|
||||||
|
|
||||||
export { HotKeysPreferences };
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
.HotKeysPreferences
|
|
||||||
display: flex;
|
|
||||||
|
|
||||||
.column
|
|
||||||
width: 50%;
|
|
||||||
padding-right: 15px;
|
|
||||||
padding-left: 15px;
|
|
||||||
|
|
||||||
.column-full
|
|
||||||
width: 100%;
|
|
||||||
padding-right: 15px;
|
|
||||||
padding-left: 15px;
|
|
||||||
@ -1,132 +0,0 @@
|
|||||||
import React, { Component } from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import { HotKeysPreferences } from './HotKeysPreferences';
|
|
||||||
import { WindowLevelPreferences } from './WindowLevelPreferences';
|
|
||||||
import { GeneralPreferences } from './GeneralPreferences';
|
|
||||||
import './UserPreferences.styl';
|
|
||||||
|
|
||||||
export class UserPreferences extends Component {
|
|
||||||
static defaultProps = {
|
|
||||||
hotkeyDefinitions: [],
|
|
||||||
windowLevelData: {},
|
|
||||||
generalPreferences: {},
|
|
||||||
};
|
|
||||||
|
|
||||||
// TODO: Make this more generic. Tabs should not be restricted to these entries
|
|
||||||
static propTypes = {
|
|
||||||
hotkeyDefinitions: PropTypes.arrayOf(
|
|
||||||
PropTypes.shape({
|
|
||||||
commandName: PropTypes.string,
|
|
||||||
keys: PropTypes.arrayOf(PropTypes.string),
|
|
||||||
label: PropTypes.string,
|
|
||||||
})
|
|
||||||
).isRequired,
|
|
||||||
windowLevelData: PropTypes.object.isRequired,
|
|
||||||
generalPreferences: PropTypes.object.isRequired,
|
|
||||||
updatePropValue: PropTypes.func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
state = {
|
|
||||||
tabIndex: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
tabClick(tabIndex) {
|
|
||||||
this.setState({ tabIndex });
|
|
||||||
}
|
|
||||||
|
|
||||||
renderHotkeysTab() {
|
|
||||||
return (
|
|
||||||
<form className="form-themed themed">
|
|
||||||
<div className="form-content">
|
|
||||||
<HotKeysPreferences
|
|
||||||
hotkeyDefinitions={this.props.hotkeyDefinitions}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
renderWindowLevelTab() {
|
|
||||||
if (this.props.windowLevelData) {
|
|
||||||
return (
|
|
||||||
<form className="form-themed themed">
|
|
||||||
<div className="form-content">
|
|
||||||
<WindowLevelPreferences
|
|
||||||
windowLevelData={this.props.windowLevelData}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
renderGeneralTab() {
|
|
||||||
return (
|
|
||||||
<form className="form-themed themed">
|
|
||||||
<div className="form-content">
|
|
||||||
<GeneralPreferences
|
|
||||||
generalPreferences={this.props.generalPreferences}
|
|
||||||
updatePropValue={this.props.updatePropValue}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
renderTabs(tabIndex) {
|
|
||||||
switch (tabIndex) {
|
|
||||||
case 0:
|
|
||||||
return this.renderHotkeysTab();
|
|
||||||
/* case 1:
|
|
||||||
return this.renderWindowLevelTab(); */
|
|
||||||
case 2:
|
|
||||||
return this.renderGeneralTab();
|
|
||||||
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getTabClass(tabIndex) {
|
|
||||||
return tabIndex === this.state.tabIndex ? 'nav-link active' : 'nav-link';
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<div className="UserPreferences">
|
|
||||||
<div className="UserPreferences__selector">
|
|
||||||
<ul className="nav nav-tabs">
|
|
||||||
<li
|
|
||||||
onClick={() => {
|
|
||||||
this.tabClick(0);
|
|
||||||
}}
|
|
||||||
className={this.getTabClass(0)}
|
|
||||||
>
|
|
||||||
<button>Hotkeys</button>
|
|
||||||
</li>
|
|
||||||
{false && (
|
|
||||||
<li
|
|
||||||
onClick={() => {
|
|
||||||
this.tabClick(1);
|
|
||||||
}}
|
|
||||||
className={this.getTabClass(1)}
|
|
||||||
>
|
|
||||||
<button>Window Level</button>
|
|
||||||
</li>
|
|
||||||
)}
|
|
||||||
<li
|
|
||||||
onClick={() => {
|
|
||||||
this.tabClick(2);
|
|
||||||
}}
|
|
||||||
className={this.getTabClass(2)}
|
|
||||||
>
|
|
||||||
<button>General</button>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
{this.renderTabs(this.state.tabIndex)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,219 +0,0 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { useSnackbarContext } from '@ohif/ui';
|
|
||||||
|
|
||||||
import './UserPreferencesForm.styl';
|
|
||||||
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
|
|
||||||
// Tabs Component wrapper
|
|
||||||
import { UserPreferencesTabs } from './UserPreferencesTabs';
|
|
||||||
|
|
||||||
// Tabs
|
|
||||||
import { HotKeysPreferences } from './HotKeysPreferences';
|
|
||||||
import { WindowLevelPreferences } from './WindowLevelPreferences';
|
|
||||||
import { GeneralPreferences } from './GeneralPreferences';
|
|
||||||
|
|
||||||
/**
|
|
||||||
@typedef TabObject
|
|
||||||
@type {Object}
|
|
||||||
@property {string} name Name for given tab
|
|
||||||
@property {ReactComponent} Component React component for given tab.
|
|
||||||
@property {object} props Props State for given tab component
|
|
||||||
@property {boolean} [hidden] To hidden tab or not
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create tabs obj.
|
|
||||||
* @returns {TabObject[]} Array of TabObjs.
|
|
||||||
*/
|
|
||||||
const createTabs = () => {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
name: 'Hotkeys',
|
|
||||||
Component: HotKeysPreferences,
|
|
||||||
props: {},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'General',
|
|
||||||
Component: GeneralPreferences,
|
|
||||||
props: {},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Window Level',
|
|
||||||
Component: WindowLevelPreferences,
|
|
||||||
props: {},
|
|
||||||
hidden: true,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Main form component to render preferences tabs and buttons
|
|
||||||
* @param {object} props component props
|
|
||||||
* @param {string} props.name Tab`s name
|
|
||||||
* @param {object} props.hotkeyDefinitions Hotkeys Data
|
|
||||||
* @param {object} props.windowLevelData Window level data
|
|
||||||
* @param {function} props.onSave Callback function when saving
|
|
||||||
* @param {function} props.onClose Callback function when closing
|
|
||||||
* @param {function} props.onResetToDefaults Callback function when resetting
|
|
||||||
*/
|
|
||||||
function UserPreferencesForm({
|
|
||||||
onClose,
|
|
||||||
onSave,
|
|
||||||
onResetToDefaults,
|
|
||||||
windowLevelData,
|
|
||||||
hotkeyDefinitions,
|
|
||||||
generalPreferences,
|
|
||||||
hotkeysManager,
|
|
||||||
defaultLanguage,
|
|
||||||
hotkeyDefaults,
|
|
||||||
}) {
|
|
||||||
const [tabs, setTabs] = useState(createTabs());
|
|
||||||
|
|
||||||
const createTabsState = (
|
|
||||||
windowLevelData,
|
|
||||||
hotkeyDefinitions,
|
|
||||||
generalPreferences
|
|
||||||
) => {
|
|
||||||
return {
|
|
||||||
Hotkeys: { hotkeyDefinitions },
|
|
||||||
'Window Level': { windowLevelData },
|
|
||||||
General: { generalPreferences },
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const [tabsState, setTabsState] = useState(
|
|
||||||
createTabsState(windowLevelData, hotkeyDefinitions, generalPreferences)
|
|
||||||
);
|
|
||||||
|
|
||||||
const [tabsError, setTabsError] = useState(
|
|
||||||
tabs.reduce((acc, tab) => {
|
|
||||||
acc[tab.name] = false;
|
|
||||||
return acc;
|
|
||||||
}, {})
|
|
||||||
);
|
|
||||||
|
|
||||||
const snackbar = useSnackbarContext();
|
|
||||||
|
|
||||||
const { t, ready: translationsAreReady } = useTranslation(
|
|
||||||
'UserPreferencesModal'
|
|
||||||
);
|
|
||||||
|
|
||||||
const onTabStateChanged = (tabName, newState) => {
|
|
||||||
setTabsState({ ...tabsState, [tabName]: newState });
|
|
||||||
};
|
|
||||||
|
|
||||||
const onTabErrorChanged = (tabName, hasError) => {
|
|
||||||
setTabsError({ ...tabsError, [tabName]: hasError });
|
|
||||||
};
|
|
||||||
|
|
||||||
const hasAnyError = () => {
|
|
||||||
return Object.values(tabsError).reduce((acc, value) => acc || value);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onResetPreferences = () => {
|
|
||||||
const defaultHotKeyDefitions = {};
|
|
||||||
|
|
||||||
hotkeyDefaults.map(item => {
|
|
||||||
const { commandName, ...values } = item;
|
|
||||||
defaultHotKeyDefitions[commandName] = { ...values };
|
|
||||||
});
|
|
||||||
|
|
||||||
// update local state
|
|
||||||
setTabsState({
|
|
||||||
...tabsState,
|
|
||||||
Hotkeys: { hotkeyDefinitions: defaultHotKeyDefitions },
|
|
||||||
General: { generalPreferences: { language: defaultLanguage } },
|
|
||||||
});
|
|
||||||
|
|
||||||
// update tabs state
|
|
||||||
setTabs(createTabs(windowLevelData, hotkeyDefinitions, generalPreferences));
|
|
||||||
|
|
||||||
// reset errors
|
|
||||||
setTabsError(
|
|
||||||
tabs.reduce((acc, tab) => {
|
|
||||||
acc[tab.name] = false;
|
|
||||||
return acc;
|
|
||||||
}, {})
|
|
||||||
);
|
|
||||||
|
|
||||||
snackbar.show({
|
|
||||||
message: (
|
|
||||||
<div dangerouslySetInnerHTML={{ __html: t('ResetDefaultMessage') }} />
|
|
||||||
),
|
|
||||||
type: 'info',
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const onSavePreferences = event => {
|
|
||||||
const toSave = Object.values(tabsState).reduce((acc, tabState) => {
|
|
||||||
return { ...acc, ...tabState };
|
|
||||||
}, {});
|
|
||||||
|
|
||||||
onSave(toSave);
|
|
||||||
snackbar.show({
|
|
||||||
message: t('SaveMessage'),
|
|
||||||
type: 'success',
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// update local state if prop values changes
|
|
||||||
useEffect(() => {
|
|
||||||
setTabsState(
|
|
||||||
createTabsState(windowLevelData, hotkeyDefinitions, generalPreferences)
|
|
||||||
);
|
|
||||||
}, [windowLevelData, hotkeyDefinitions, generalPreferences]);
|
|
||||||
|
|
||||||
return translationsAreReady ? (
|
|
||||||
<div className="UserPreferencesForm">
|
|
||||||
<UserPreferencesTabs
|
|
||||||
tabs={tabs}
|
|
||||||
tabsState={tabsState}
|
|
||||||
tabsError={tabsError}
|
|
||||||
onTabStateChanged={onTabStateChanged}
|
|
||||||
onTabErrorChanged={onTabErrorChanged}
|
|
||||||
/>
|
|
||||||
<div className="footer">
|
|
||||||
<button
|
|
||||||
className="btn btn-danger pull-left"
|
|
||||||
data-cy="reset-default-btn"
|
|
||||||
onClick={onResetPreferences}
|
|
||||||
>
|
|
||||||
{t('Reset to Defaults')}
|
|
||||||
</button>
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
onClick={onClose}
|
|
||||||
data-cy="cancel-btn"
|
|
||||||
className="btn btn-default"
|
|
||||||
>
|
|
||||||
{t('Cancel')}
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
className="btn btn-primary"
|
|
||||||
data-cy="save-btn"
|
|
||||||
disabled={hasAnyError()}
|
|
||||||
onClick={onSavePreferences}
|
|
||||||
>
|
|
||||||
{t('Save')}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
UserPreferencesForm.propTypes = {
|
|
||||||
onClose: PropTypes.func,
|
|
||||||
onSave: PropTypes.func,
|
|
||||||
onResetToDefaults: PropTypes.func,
|
|
||||||
windowLevelData: PropTypes.object,
|
|
||||||
hotkeyDefinitions: PropTypes.object,
|
|
||||||
generalPreferences: PropTypes.object,
|
|
||||||
hotkeysManager: PropTypes.object,
|
|
||||||
defaultLanguage: PropTypes.string,
|
|
||||||
hotkeyDefaults: PropTypes.array,
|
|
||||||
};
|
|
||||||
|
|
||||||
export { UserPreferencesForm };
|
|
||||||
@ -1,23 +0,0 @@
|
|||||||
@import './../../design/styles/common/navbar.styl'
|
|
||||||
@import './../../design/styles/common/global.styl'
|
|
||||||
@import './../../design/styles/common/modal.styl'
|
|
||||||
@import './../../design/styles/common/button.styl'
|
|
||||||
|
|
||||||
.close
|
|
||||||
float: right;
|
|
||||||
font-size: 21px;
|
|
||||||
font-weight: 700;
|
|
||||||
line-height: 1;
|
|
||||||
color: #000;
|
|
||||||
text-shadow: 0 1px 0 #fff;
|
|
||||||
opacity: .2;
|
|
||||||
|
|
||||||
.UserPreferencesForm
|
|
||||||
.footer
|
|
||||||
display: flex
|
|
||||||
flex-direction: row
|
|
||||||
justify-content: space-between
|
|
||||||
|
|
||||||
div
|
|
||||||
button:last-child
|
|
||||||
margin-left: 10px
|
|
||||||
@ -1,107 +0,0 @@
|
|||||||
import React, { useState } from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import './UserPreferencesTabs.styl';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Render tab component
|
|
||||||
* @param {object} tab TabObject containing tab data
|
|
||||||
* @param {function} onTabStateChanged Callback Function in case tab changes its state
|
|
||||||
* @param {function} onTabErrorChanged Callback Function in case any error on tab
|
|
||||||
*/
|
|
||||||
const renderTab = (
|
|
||||||
tab = {},
|
|
||||||
tabsState,
|
|
||||||
tabsError,
|
|
||||||
onTabStateChanged,
|
|
||||||
onTabErrorChanged
|
|
||||||
) => {
|
|
||||||
const { props, Component, name, hidden = false } = tab;
|
|
||||||
|
|
||||||
const tabState = tabsState[name];
|
|
||||||
const tabError = tabsError[name];
|
|
||||||
|
|
||||||
return !hidden ? (
|
|
||||||
<form className="form-themed themed tabs">
|
|
||||||
<div className="form-content">
|
|
||||||
<Component
|
|
||||||
key={name}
|
|
||||||
{...tabState}
|
|
||||||
{...props}
|
|
||||||
name={name}
|
|
||||||
tabError={tabError}
|
|
||||||
onTabStateChanged={onTabStateChanged}
|
|
||||||
onTabErrorChanged={onTabErrorChanged}
|
|
||||||
></Component>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
) : null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderTabsHeader = (tabs, activeTabIndex, onHeaderChanged) => {
|
|
||||||
return tabs.length > 0
|
|
||||||
? tabs.map((tab, index) => {
|
|
||||||
const { name, hidden = false } = tab;
|
|
||||||
|
|
||||||
const cypressSelectorId = name.toLowerCase();
|
|
||||||
const tabClass =
|
|
||||||
index === activeTabIndex ? 'nav-link active' : 'nav-link';
|
|
||||||
return !hidden ? (
|
|
||||||
<li
|
|
||||||
key={name}
|
|
||||||
onClick={() => {
|
|
||||||
onHeaderChanged(index);
|
|
||||||
}}
|
|
||||||
className={tabClass}
|
|
||||||
data-cy={cypressSelectorId}
|
|
||||||
>
|
|
||||||
<button>{name}</button>
|
|
||||||
</li>
|
|
||||||
) : null;
|
|
||||||
})
|
|
||||||
: null;
|
|
||||||
};
|
|
||||||
/**
|
|
||||||
* Component to render tabs based on currentActiveTabIndex
|
|
||||||
*
|
|
||||||
* In case any tab changes its state this current component tells parent through function callback
|
|
||||||
* @param {object} props Component props
|
|
||||||
*/
|
|
||||||
function UserPreferencesTabs({
|
|
||||||
tabs,
|
|
||||||
tabsState,
|
|
||||||
tabsError,
|
|
||||||
onTabStateChanged,
|
|
||||||
onTabErrorChanged,
|
|
||||||
}) {
|
|
||||||
const [activeTabIndex, setActiveTabIndex] = useState(0);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="UserPreferencesTabs">
|
|
||||||
<div className="UserPreferencesTabs__selector">
|
|
||||||
<div className="dialog-separator-after">
|
|
||||||
<ul className="nav nav-tabs">
|
|
||||||
{renderTabsHeader(tabs, activeTabIndex, setActiveTabIndex)}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{renderTab(
|
|
||||||
tabs[activeTabIndex],
|
|
||||||
tabsState,
|
|
||||||
tabsError,
|
|
||||||
onTabStateChanged,
|
|
||||||
onTabErrorChanged
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
UserPreferencesTabs.propTypes = {
|
|
||||||
tabs: PropTypes.array.isRequired,
|
|
||||||
tabsState: PropTypes.object.isRequired,
|
|
||||||
tabsError: PropTypes.object.isRequired,
|
|
||||||
onTabStateChanged: PropTypes.func.isRequired,
|
|
||||||
onTabErrorChanged: PropTypes.func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export { UserPreferencesTabs };
|
|
||||||
@ -1,29 +0,0 @@
|
|||||||
@import './../../design/styles/common/form.styl'
|
|
||||||
@import './../../design/styles/common/navbar.styl'
|
|
||||||
@import './../../design/styles/common/state.styl'
|
|
||||||
@import './../../design/styles/common/global.styl'
|
|
||||||
|
|
||||||
.UserPreferencesTabs
|
|
||||||
display: flex
|
|
||||||
flex-direction: column
|
|
||||||
|
|
||||||
&__selector
|
|
||||||
border-bottom: 3px solid black
|
|
||||||
|
|
||||||
.errorMessage
|
|
||||||
color: var(--state-error-text)
|
|
||||||
font-size: 10px
|
|
||||||
text-transform: uppercase;
|
|
||||||
|
|
||||||
.form-content
|
|
||||||
border-bottom: 3px solid var(--primary-background-color)
|
|
||||||
margin-bottom: 20px
|
|
||||||
margin-left: -20px
|
|
||||||
margin-right: -20px
|
|
||||||
max-height: 70vh
|
|
||||||
overflow-y: auto
|
|
||||||
padding: 20px
|
|
||||||
min-height: 500px
|
|
||||||
|
|
||||||
.popover
|
|
||||||
width: 300px
|
|
||||||
@ -1,132 +0,0 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import './WindowLevelPreferences.styl';
|
|
||||||
/**
|
|
||||||
* WindowLevelPreferencesRow
|
|
||||||
* Renders row for window level preference
|
|
||||||
* It stores current state and whenever it changes, component messages parent of new value (through function callback)
|
|
||||||
* @param {object} props component props
|
|
||||||
* @param {string} props.description description for given preset
|
|
||||||
* @param {number} props.window window value
|
|
||||||
* @param {number} props.level level value
|
|
||||||
* @param {string} props.rowName name of given row to identify it
|
|
||||||
* @param {function} props.onSuccessChanged Callback function to communicate parent in case its states changes
|
|
||||||
*/
|
|
||||||
function WindowLevelPreferencesRow({
|
|
||||||
description,
|
|
||||||
window,
|
|
||||||
level,
|
|
||||||
rowName,
|
|
||||||
onSuccessChanged,
|
|
||||||
// onFailureChanged
|
|
||||||
}) {
|
|
||||||
const [rowState, setRowState] = useState({ description, window, level });
|
|
||||||
|
|
||||||
const onInputChanged = (event, name) => {
|
|
||||||
const newValue = event.target.value;
|
|
||||||
setRowState({ ...rowState, [name]: newValue });
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
onSuccessChanged(rowName, rowState);
|
|
||||||
}, [rowState]);
|
|
||||||
|
|
||||||
const renderTd = (value, name, type) => {
|
|
||||||
return (
|
|
||||||
<td className="p-r-1">
|
|
||||||
<label className="wrapperLabel">
|
|
||||||
<input
|
|
||||||
value={value}
|
|
||||||
type={type}
|
|
||||||
className="form-control"
|
|
||||||
onChange={event => {
|
|
||||||
onInputChanged(event, name);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
</td>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<tr key={rowName}>
|
|
||||||
<td className="p-r-1 text-center">{rowName}</td>
|
|
||||||
{renderTd(rowState.description, 'description', 'text')}
|
|
||||||
{renderTd(rowState.window, 'window', 'number')}
|
|
||||||
{renderTd(rowState.level, 'level', 'number')}
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
WindowLevelPreferencesRow.propTypes = {
|
|
||||||
description: PropTypes.string.isRequired,
|
|
||||||
window: PropTypes.number.isRequired,
|
|
||||||
level: PropTypes.number.isRequired,
|
|
||||||
rowName: PropTypes.string.isRequired,
|
|
||||||
onSuccessChanged: PropTypes.func.isRequired,
|
|
||||||
//onFailureChanged: PropTypes.func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* WindowLevelPreferences tab
|
|
||||||
* It renders all window level presets
|
|
||||||
*
|
|
||||||
* It stores current state and whenever it changes, component messages parent of new value (through function callback)
|
|
||||||
* @param {object} props component props
|
|
||||||
* @param {string} props.name Tab`s name
|
|
||||||
* @param {object} props.windowLevelData Data for initial state
|
|
||||||
* @param {function} props.onTabStateChanged Callback function to communicate parent in case its states changes
|
|
||||||
*/
|
|
||||||
function WindowLevelPreferences({
|
|
||||||
windowLevelData,
|
|
||||||
name,
|
|
||||||
onTabStateChanged /*onTabErrorChanged*/,
|
|
||||||
}) {
|
|
||||||
const [tabState, setTabState] = useState(windowLevelData);
|
|
||||||
// TODO to be used once error handling is implemented
|
|
||||||
//const [tabError, setTabError] = useState(false);
|
|
||||||
|
|
||||||
const onWindowLevelChanged = (key, state) => {
|
|
||||||
setTabState({ ...tabState, [key]: state });
|
|
||||||
};
|
|
||||||
|
|
||||||
// tell parent to update its state
|
|
||||||
useEffect(() => {
|
|
||||||
onTabStateChanged(name, { windowLevelData: tabState });
|
|
||||||
}, [tabState]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<table className="full-width">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th className="p-x-1 text-center presetIndex">Preset</th>
|
|
||||||
<th className="p-x-1">Description</th>
|
|
||||||
<th className="p-x-1">Window</th>
|
|
||||||
<th className="p-x-1">Level</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{Object.keys(tabState).map(objKey => (
|
|
||||||
<WindowLevelPreferencesRow
|
|
||||||
onSuccessChanged={onWindowLevelChanged}
|
|
||||||
rowName={objKey}
|
|
||||||
key={objKey}
|
|
||||||
description={tabState[objKey].description}
|
|
||||||
window={tabState[objKey].window}
|
|
||||||
level={tabState[objKey].level}
|
|
||||||
></WindowLevelPreferencesRow>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
WindowLevelPreferences.propTypes = {
|
|
||||||
windowLevelData: PropTypes.object.isRequired,
|
|
||||||
name: PropTypes.string.isRequired,
|
|
||||||
onTabStateChanged: PropTypes.func.isRequired,
|
|
||||||
//onTabErrorChanged: PropTypes.func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export { WindowLevelPreferences };
|
|
||||||
@ -1,4 +0,0 @@
|
|||||||
@import './UserPreferencesTabs.styl'
|
|
||||||
|
|
||||||
.presetIndex
|
|
||||||
padding: 0px 10px 0px 10px
|
|
||||||
@ -1,14 +0,0 @@
|
|||||||
export default {
|
|
||||||
currentLanguage: 'en',
|
|
||||||
languages: [
|
|
||||||
{
|
|
||||||
value: 'en',
|
|
||||||
label: 'English',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'es',
|
|
||||||
label: 'Spanish',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
onChange: language => {},
|
|
||||||
};
|
|
||||||
@ -1,75 +0,0 @@
|
|||||||
export default {
|
|
||||||
defaultTool: { label: 'Default Tool', keys: ['ESC'], column: 0 },
|
|
||||||
zoom: { label: 'Zoom', keys: ['Z'], column: 0 },
|
|
||||||
wwwc: { label: 'W/L', keys: ['W'], column: 0 },
|
|
||||||
pan: { label: 'Pan', keys: ['P'], column: 0 },
|
|
||||||
angle: { label: 'Angle measurement', keys: ['A'], column: 0 },
|
|
||||||
stackScroll: { label: 'Scroll stack', keys: ['S'], column: 0 },
|
|
||||||
magnify: { label: 'Magnify', keys: ['M'], column: 0 },
|
|
||||||
length: { label: 'Length measurement', keys: [''], column: 0 },
|
|
||||||
annotate: { label: 'Annotate', keys: [''], column: 0 },
|
|
||||||
dragProbe: { label: 'Pixel probe', keys: [''], column: 0 },
|
|
||||||
ellipticalRoi: { label: 'Elliptical ROI', keys: [''], column: 0 },
|
|
||||||
rectangleRoi: { label: 'Rectangle ROI', keys: [''], column: 0 },
|
|
||||||
|
|
||||||
// Viewport hotkeys
|
|
||||||
flipH: { label: 'Flip Horizontally', keys: ['H'], column: 0 },
|
|
||||||
flipV: { label: 'Flip Vertically', keys: ['V'], column: 0 },
|
|
||||||
rotateR: { label: 'Rotate Right', keys: ['R'], column: 0 },
|
|
||||||
rotateL: { label: 'Rotate Left', keys: ['L'], column: 0 },
|
|
||||||
invert: { label: 'Invert', keys: ['I'], column: 0 },
|
|
||||||
zoomIn: { label: 'Zoom In', keys: [''], column: 0 },
|
|
||||||
zoomOut: { label: 'Zoom Out', keys: [''], column: 0 },
|
|
||||||
zoomToFit: { label: 'Zoom to Fit', keys: [''], column: 0 },
|
|
||||||
resetViewport: { label: 'Reset', keys: [''], column: 0 },
|
|
||||||
clearTools: { label: 'Clear Tools', keys: [''], column: 0 },
|
|
||||||
|
|
||||||
// 2nd column
|
|
||||||
|
|
||||||
// Viewport navigation hotkeys
|
|
||||||
scrollDown: { label: 'Scroll Down', keys: ['DOWN'], column: 1 },
|
|
||||||
scrollUp: { label: 'Scroll Up', keys: ['UP'], column: 1 },
|
|
||||||
scrollLastImage: { label: 'Scroll to Last Image', keys: ['END'], column: 1 },
|
|
||||||
scrollFirstImage: {
|
|
||||||
label: 'Scroll to First Image',
|
|
||||||
keys: ['HOME'],
|
|
||||||
column: 1,
|
|
||||||
},
|
|
||||||
previousDisplaySet: {
|
|
||||||
label: 'Previous Series',
|
|
||||||
keys: ['PAGEUP'],
|
|
||||||
column: 1,
|
|
||||||
},
|
|
||||||
nextDisplaySet: { label: 'Next Series', keys: ['PAGEDOWN'], column: 1 },
|
|
||||||
nextPanel: { label: 'Next Image Viewport', keys: ['RIGHT'], column: 1 },
|
|
||||||
previousPanel: {
|
|
||||||
label: 'Previous Image Viewport',
|
|
||||||
keys: ['LEFT'],
|
|
||||||
column: 1,
|
|
||||||
},
|
|
||||||
|
|
||||||
// Miscellaneous hotkeys
|
|
||||||
toggleOverlayTags: {
|
|
||||||
label: 'Toggle Image Info Overlay',
|
|
||||||
keys: ['O'],
|
|
||||||
column: 1,
|
|
||||||
},
|
|
||||||
toggleCinePlay: { label: 'Play/Pause Cine', keys: ['SPACE'], column: 1 },
|
|
||||||
toggleCineDialog: {
|
|
||||||
label: 'Show/Hide Cine Controls',
|
|
||||||
keys: [''],
|
|
||||||
column: 1,
|
|
||||||
},
|
|
||||||
|
|
||||||
// Preset hotkeys
|
|
||||||
WLPreset0: { label: 'W/L Preset 0 (Soft Tissue)', keys: ['1'], column: 1 },
|
|
||||||
WLPreset1: { label: 'W/L Preset 1 (Lung)', keys: ['2'], column: 1 },
|
|
||||||
WLPreset2: { label: 'W/L Preset 2 (Liver)', keys: ['3'], column: 1 },
|
|
||||||
WLPreset3: { label: 'W/L Preset 3 (Bone)', keys: ['4'], column: 1 },
|
|
||||||
WLPreset4: { label: 'W/L Preset 4 (Brain)', keys: ['5'], column: 1 },
|
|
||||||
WLPreset5: { label: 'W/L Preset 5', keys: ['6'], column: 1 },
|
|
||||||
WLPreset6: { label: 'W/L Preset 6', keys: ['7'], column: 1 },
|
|
||||||
WLPreset7: { label: 'W/L Preset 7', keys: ['8'], column: 1 },
|
|
||||||
WLPreset8: { label: 'W/L Preset 8', keys: ['9'], column: 1 },
|
|
||||||
WLPreset9: { label: 'W/L Preset 0', keys: ['0'], column: 1 },
|
|
||||||
};
|
|
||||||
@ -1,54 +0,0 @@
|
|||||||
---
|
|
||||||
name: User Preferences Form
|
|
||||||
menu: Components
|
|
||||||
route: /components/user-preferences-form
|
|
||||||
---
|
|
||||||
|
|
||||||
import { Playground, Props } from 'docz'
|
|
||||||
import { State } from 'react-powerplug'
|
|
||||||
import { UserPreferencesForm } from './../index.js'
|
|
||||||
import NameSpace from '../../../__docs__/NameSpace'
|
|
||||||
//
|
|
||||||
import windowLevelDefaults from './windowLevelDefaults.js'
|
|
||||||
import hotkeyDefaults from './hotkeyDefaults.js'
|
|
||||||
|
|
||||||
# User Preferences Form
|
|
||||||
|
|
||||||
## Basic usage
|
|
||||||
|
|
||||||
<Playground>
|
|
||||||
<State initial={{
|
|
||||||
isOpen: false,
|
|
||||||
windowLevelData: windowLevelDefaults,
|
|
||||||
hotkeyDefinitions: hotkeyDefaults,
|
|
||||||
}}>
|
|
||||||
|
|
||||||
{({ state, setState }) => (
|
|
||||||
<React.Fragment>
|
|
||||||
<button
|
|
||||||
className="btn btn-primary"
|
|
||||||
type="button"
|
|
||||||
onClick={ () => setState({ isOpen: true }) }
|
|
||||||
>
|
|
||||||
Open user preferences
|
|
||||||
</button>
|
|
||||||
<UserPreferencesForm
|
|
||||||
{...state}
|
|
||||||
onCancel={() => setState({ isOpen: false })}
|
|
||||||
onSave={() => alert('on save')}
|
|
||||||
onResetToDefaults={() => alert('on reset')}
|
|
||||||
/>
|
|
||||||
</React.Fragment>
|
|
||||||
)}
|
|
||||||
|
|
||||||
</State>
|
|
||||||
|
|
||||||
</Playground>
|
|
||||||
|
|
||||||
## API
|
|
||||||
|
|
||||||
<Props of={UserPreferencesForm} />
|
|
||||||
|
|
||||||
## Translation Namespace
|
|
||||||
|
|
||||||
<NameSpace name="UserPreferencesForm" />
|
|
||||||
@ -1,13 +0,0 @@
|
|||||||
export default {
|
|
||||||
0: { description: 'Soft tissue', window: 400, level: 40 },
|
|
||||||
1: { description: 'Lung', window: 1500, level: -600 },
|
|
||||||
2: { description: 'Liver', window: 150, level: 90 },
|
|
||||||
3: { description: 'Bone', window: 2500, level: 480 },
|
|
||||||
4: { description: 'Brain', window: 80, level: 40 },
|
|
||||||
5: { description: '', window: '', level: '' },
|
|
||||||
6: { description: '', window: '', level: '' },
|
|
||||||
7: { description: '', window: '', level: '' },
|
|
||||||
8: { description: '', window: '', level: '' },
|
|
||||||
9: { description: '', window: '', level: '' },
|
|
||||||
10: { description: '', window: '', level: '' },
|
|
||||||
};
|
|
||||||
@ -1,4 +0,0 @@
|
|||||||
export { UserPreferencesTabs } from './UserPreferencesTabs.js';
|
|
||||||
export { AboutContent } from '../content/aboutContent/AboutContent.js';
|
|
||||||
export { UserPreferencesForm } from './UserPreferencesForm.js';
|
|
||||||
export { GeneralPreferences } from './GeneralPreferences.js';
|
|
||||||
@ -19,13 +19,15 @@ import {
|
|||||||
TableList,
|
TableList,
|
||||||
TableListItem,
|
TableListItem,
|
||||||
Thumbnail,
|
Thumbnail,
|
||||||
|
TabComponents,
|
||||||
|
TabFooter,
|
||||||
|
HotkeyField,
|
||||||
|
LanguageSwitcher,
|
||||||
TableSearchFilter,
|
TableSearchFilter,
|
||||||
TablePagination,
|
TablePagination,
|
||||||
ToolbarSection,
|
ToolbarSection,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
AboutContent,
|
AboutContent,
|
||||||
UserPreferences,
|
|
||||||
UserPreferencesForm,
|
|
||||||
OHIFModal,
|
OHIFModal,
|
||||||
} from './components';
|
} from './components';
|
||||||
import { useDebounce, useMedia } from './hooks';
|
import { useDebounce, useMedia } from './hooks';
|
||||||
@ -98,6 +100,10 @@ export {
|
|||||||
TableList,
|
TableList,
|
||||||
TableListItem,
|
TableListItem,
|
||||||
Thumbnail,
|
Thumbnail,
|
||||||
|
TabComponents,
|
||||||
|
TabFooter,
|
||||||
|
HotkeyField,
|
||||||
|
LanguageSwitcher,
|
||||||
TableSearchFilter,
|
TableSearchFilter,
|
||||||
TablePagination,
|
TablePagination,
|
||||||
Toolbar,
|
Toolbar,
|
||||||
@ -105,8 +111,6 @@ export {
|
|||||||
ToolbarSection,
|
ToolbarSection,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
AboutContent,
|
AboutContent,
|
||||||
UserPreferences,
|
|
||||||
UserPreferencesForm,
|
|
||||||
ViewerbaseDragDropContext,
|
ViewerbaseDragDropContext,
|
||||||
SnackbarProvider,
|
SnackbarProvider,
|
||||||
useSnackbarContext,
|
useSnackbarContext,
|
||||||
|
|||||||
@ -30,9 +30,8 @@ describe('OHIF User Preferences', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('checks translation by selecting Spanish language', function() {
|
it('checks translation by selecting Spanish language', function() {
|
||||||
cy.get('@userPreferencesGeneralTab')
|
cy.changePreferencesTab('@userPreferencesGeneralTab');
|
||||||
.click()
|
cy.get('@userPreferencesGeneralTab').should('have.class', 'active');
|
||||||
.should('have.class', 'active');
|
|
||||||
|
|
||||||
// Language dropdown should be displayed
|
// Language dropdown should be displayed
|
||||||
cy.get('#language-select').should('be.visible');
|
cy.get('#language-select').should('be.visible');
|
||||||
@ -65,14 +64,14 @@ describe('OHIF User Preferences', () => {
|
|||||||
cy.get('[data-cy="options-menu"]').click();
|
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
|
// Set language to English and save
|
||||||
cy.setLanguage('English');
|
cy.setLanguage('English (USA)');
|
||||||
|
|
||||||
// Set language to Spanish and cancel
|
// Set language to Spanish and cancel
|
||||||
cy.setLanguage('Spanish', false);
|
cy.setLanguage('Spanish', false);
|
||||||
|
|
||||||
// Header should be kept in English
|
// Header should be kept in "English (USA)"
|
||||||
cy.get('.research-use')
|
cy.get('.research-use')
|
||||||
.scrollIntoView()
|
.scrollIntoView()
|
||||||
.should('have.text', 'INVESTIGATIONAL USE ONLY');
|
.should('have.text', 'INVESTIGATIONAL USE ONLY');
|
||||||
@ -93,7 +92,7 @@ describe('OHIF User Preferences', () => {
|
|||||||
cy.get('[data-cy="options-menu"]').click();
|
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
|
// Set language to Spanish
|
||||||
cy.setLanguage('Spanish');
|
cy.setLanguage('Spanish');
|
||||||
|
|
||||||
@ -101,7 +100,7 @@ describe('OHIF User Preferences', () => {
|
|||||||
cy.openPreferences();
|
cy.openPreferences();
|
||||||
|
|
||||||
// Go to general tab
|
// Go to general tab
|
||||||
cy.get('@userPreferencesGeneralTab').click();
|
cy.changePreferencesTab('@userPreferencesGeneralTab');
|
||||||
|
|
||||||
cy.get('@restoreBtn')
|
cy.get('@restoreBtn')
|
||||||
.scrollIntoView()
|
.scrollIntoView()
|
||||||
@ -112,12 +111,12 @@ describe('OHIF User Preferences', () => {
|
|||||||
.scrollIntoView()
|
.scrollIntoView()
|
||||||
.click();
|
.click();
|
||||||
|
|
||||||
// Header should be in English
|
// Header should be in "English (USA)"
|
||||||
cy.get('.research-use')
|
cy.get('.research-use')
|
||||||
.scrollIntoView()
|
.scrollIntoView()
|
||||||
.should('have.text', 'INVESTIGATIONAL USE ONLY');
|
.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"]')
|
cy.get('[data-cy="options-menu"]')
|
||||||
.should('have.text', 'Options')
|
.should('have.text', 'Options')
|
||||||
.click();
|
.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() {
|
it('checks if Preferences set in Study List Page will be consistent on Viewer Page', function() {
|
||||||
// Go go hotkeys tab
|
// Go go hotkeys tab
|
||||||
cy.get('@userPreferencesHotkeysTab')
|
cy.changePreferencesTab('@userPreferencesHotkeysTab');
|
||||||
.click()
|
cy.get('@userPreferencesHotkeysTab').should('have.class', 'active');
|
||||||
.should('have.class', 'active');
|
|
||||||
|
|
||||||
// Set new hotkey for 'Rotate Right' function
|
// Set new hotkey for 'Rotate Right' function
|
||||||
cy.setNewHotkeyShortcutOnUserPreferencesModal('Rotate Right', '{shift}Q');
|
cy.setNewHotkeyShortcutOnUserPreferencesModal('Rotate Right', '{shift}Q');
|
||||||
@ -150,7 +148,7 @@ describe('OHIF User Preferences', () => {
|
|||||||
cy.openPreferences();
|
cy.openPreferences();
|
||||||
|
|
||||||
// Go to General tab
|
// Go to General tab
|
||||||
cy.get('@userPreferencesGeneralTab').click();
|
cy.changePreferencesTab('@userPreferencesGeneralTab');
|
||||||
|
|
||||||
// Set language to Spanish
|
// Set language to Spanish
|
||||||
cy.setLanguage('Spanish');
|
cy.setLanguage('Spanish');
|
||||||
@ -221,9 +219,8 @@ describe('OHIF User Preferences', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('checks translation by selecting Spanish language', function() {
|
it('checks translation by selecting Spanish language', function() {
|
||||||
cy.get('@userPreferencesGeneralTab')
|
cy.changePreferencesTab('@userPreferencesGeneralTab');
|
||||||
.click()
|
cy.get('@userPreferencesGeneralTab').should('have.class', 'active');
|
||||||
.should('have.class', 'active');
|
|
||||||
|
|
||||||
// Language dropdown should be displayed
|
// Language dropdown should be displayed
|
||||||
cy.get('#language-select').should('be.visible');
|
cy.get('#language-select').should('be.visible');
|
||||||
@ -256,14 +253,14 @@ describe('OHIF User Preferences', () => {
|
|||||||
cy.get('[data-cy="options-menu"]').click();
|
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
|
// Set language to English and save
|
||||||
cy.setLanguage('English');
|
cy.setLanguage('English (USA)');
|
||||||
|
|
||||||
// Set language to Spanish and cancel
|
// Set language to Spanish and cancel
|
||||||
cy.setLanguage('Spanish', false);
|
cy.setLanguage('Spanish', false);
|
||||||
|
|
||||||
// Header should be kept in English
|
// Header should be kept in "English (USA)"
|
||||||
cy.get('.research-use')
|
cy.get('.research-use')
|
||||||
.scrollIntoView()
|
.scrollIntoView()
|
||||||
.should('have.text', 'INVESTIGATIONAL USE ONLY');
|
.should('have.text', 'INVESTIGATIONAL USE ONLY');
|
||||||
@ -283,10 +280,9 @@ describe('OHIF User Preferences', () => {
|
|||||||
cy.get('[data-cy="options-menu"]').click();
|
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() {
|
||||||
cy.get('@userPreferencesGeneralTab')
|
cy.changePreferencesTab('@userPreferencesGeneralTab');
|
||||||
.click()
|
cy.get('@userPreferencesGeneralTab').should('have.class', 'active');
|
||||||
.should('have.class', 'active');
|
|
||||||
|
|
||||||
// Language dropdown should be displayed
|
// Language dropdown should be displayed
|
||||||
cy.get('#language-select').should('be.visible');
|
cy.get('#language-select').should('be.visible');
|
||||||
@ -298,7 +294,7 @@ describe('OHIF User Preferences', () => {
|
|||||||
cy.openPreferences();
|
cy.openPreferences();
|
||||||
|
|
||||||
// Go to general tab
|
// Go to general tab
|
||||||
cy.get('@userPreferencesGeneralTab').click();
|
cy.changePreferencesTab('@userPreferencesGeneralTab');
|
||||||
|
|
||||||
cy.get('@restoreBtn')
|
cy.get('@restoreBtn')
|
||||||
.scrollIntoView()
|
.scrollIntoView()
|
||||||
@ -308,12 +304,12 @@ describe('OHIF User Preferences', () => {
|
|||||||
.scrollIntoView()
|
.scrollIntoView()
|
||||||
.click();
|
.click();
|
||||||
|
|
||||||
// Header should be in English
|
// Header should be in "English (USA)""
|
||||||
cy.get('.research-use')
|
cy.get('.research-use')
|
||||||
.scrollIntoView()
|
.scrollIntoView()
|
||||||
.should('have.text', 'INVESTIGATIONAL USE ONLY');
|
.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"]')
|
cy.get('[data-cy="options-menu"]')
|
||||||
.should('have.text', 'Options')
|
.should('have.text', 'Options')
|
||||||
.click();
|
.click();
|
||||||
@ -330,9 +326,8 @@ describe('OHIF User Preferences', () => {
|
|||||||
|
|
||||||
it('checks new hotkeys for "Rotate Right" and "Rotate Left"', function() {
|
it('checks new hotkeys for "Rotate Right" and "Rotate Left"', function() {
|
||||||
// Go go hotkeys tab
|
// Go go hotkeys tab
|
||||||
cy.get('@userPreferencesHotkeysTab')
|
cy.changePreferencesTab('@userPreferencesHotkeysTab');
|
||||||
.click()
|
cy.get('@userPreferencesHotkeysTab').should('have.class', 'active');
|
||||||
.should('have.class', 'active');
|
|
||||||
|
|
||||||
// Set new hotkey for 'Rotate Right' function
|
// Set new hotkey for 'Rotate Right' function
|
||||||
cy.setNewHotkeyShortcutOnUserPreferencesModal(
|
cy.setNewHotkeyShortcutOnUserPreferencesModal(
|
||||||
@ -360,24 +355,17 @@ describe('OHIF User Preferences', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('checks new hotkeys for "Next" and "Previous" Image on Viewport', function() {
|
it('checks new hotkeys for "Next" and "Previous" Image on Viewport', function() {
|
||||||
// Go go hotkeys tab
|
// Update hotkeys for 'Next/Previous Viewport'
|
||||||
cy.get('@userPreferencesHotkeysTab')
|
cy.changePreferencesTab('@userPreferencesHotkeysTab');
|
||||||
.click()
|
cy.get('@userPreferencesHotkeysTab').should('have.class', 'active');
|
||||||
.should('have.class', 'active');
|
|
||||||
|
|
||||||
// Set new hotkey for 'Next Image Viewport' function
|
|
||||||
cy.setNewHotkeyShortcutOnUserPreferencesModal(
|
cy.setNewHotkeyShortcutOnUserPreferencesModal(
|
||||||
'Next Image Viewport',
|
'Next Viewport',
|
||||||
'{shift}{rightarrow}'
|
'{shift}{rightarrow}'
|
||||||
);
|
);
|
||||||
|
|
||||||
// Set new hotkey for 'Previous Image Viewport' function
|
|
||||||
cy.setNewHotkeyShortcutOnUserPreferencesModal(
|
cy.setNewHotkeyShortcutOnUserPreferencesModal(
|
||||||
'Previous Image Viewport',
|
'Previous Viewport',
|
||||||
'{shift}{leftarrow}'
|
'{shift}{leftarrow}'
|
||||||
);
|
);
|
||||||
|
|
||||||
// Save new hotkeys
|
|
||||||
cy.get('@saveBtn')
|
cy.get('@saveBtn')
|
||||||
.scrollIntoView()
|
.scrollIntoView()
|
||||||
.click();
|
.click();
|
||||||
@ -386,20 +374,25 @@ describe('OHIF User Preferences', () => {
|
|||||||
cy.setLayout(3, 1);
|
cy.setLayout(3, 1);
|
||||||
cy.waitViewportImageLoading();
|
cy.waitViewportImageLoading();
|
||||||
|
|
||||||
// Rotate Right and Invert colors on Viewport #1
|
// Reset, Rotate Right and Invert colors on Viewport #1
|
||||||
cy.get('body').type('RI');
|
cy.get('body').type(' ');
|
||||||
// Check that image was rotated
|
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');
|
cy.get('@viewportInfoMidTop').should('contains.text', 'R');
|
||||||
|
|
||||||
//Move to Next Viewport
|
// Verify 2nd viewport was rotated
|
||||||
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
|
|
||||||
cy.get(
|
cy.get(
|
||||||
':nth-child(2) > .viewport-wrapper > .viewport-element > .ViewportOrientationMarkers.noselect > .top-mid.orientation-marker'
|
':nth-child(2) > .viewport-wrapper > .viewport-element > .ViewportOrientationMarkers.noselect > .top-mid.orientation-marker'
|
||||||
).as('viewport2InfoMidTop');
|
).as('viewport2InfoMidTop');
|
||||||
// Check that image was rotated
|
|
||||||
cy.get('@viewport2InfoMidTop').should('contains.text', 'P');
|
cy.get('@viewport2InfoMidTop').should('contains.text', 'P');
|
||||||
|
|
||||||
//Move to Previous Viewport
|
//Move to Previous Viewport
|
||||||
@ -417,24 +410,18 @@ describe('OHIF User Preferences', () => {
|
|||||||
|
|
||||||
it('checks error message when duplicated hotkeys are inserted', function() {
|
it('checks error message when duplicated hotkeys are inserted', function() {
|
||||||
// Go go hotkeys tab
|
// Go go hotkeys tab
|
||||||
cy.get('@userPreferencesHotkeysTab').click();
|
cy.changePreferencesTab('@userPreferencesHotkeysTab');
|
||||||
|
|
||||||
// Set duplicated hotkey for 'Rotate Right' function
|
// Set duplicated hotkey for 'Rotate Right' function
|
||||||
cy.setNewHotkeyShortcutOnUserPreferencesModal(
|
cy.setNewHotkeyShortcutOnUserPreferencesModal('Rotate Right', '{i}');
|
||||||
'Rotate Right',
|
|
||||||
'{rightarrow}'
|
|
||||||
);
|
|
||||||
|
|
||||||
// Check error message
|
// Check error message
|
||||||
cy.get('.HotKeysPreferences').within(() => {
|
cy.get('.HotkeysPreferences').within(() => {
|
||||||
cy.contains('Rotate Right') // label we're looking for
|
cy.contains('Rotate Right') // label we're looking for
|
||||||
.parent()
|
.parent()
|
||||||
.find('.errorMessage')
|
.find('.errorMessage')
|
||||||
.as('errorMsg')
|
.as('errorMsg')
|
||||||
.should(
|
.should('have.text', '"Invert" is already using the "i" shortcut.');
|
||||||
'have.text',
|
|
||||||
'"Next Image Viewport" is already using the "right" shortcut.'
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
//Cancel hotkeys
|
//Cancel hotkeys
|
||||||
cy.get('@cancelBtn')
|
cy.get('@cancelBtn')
|
||||||
@ -444,13 +431,13 @@ describe('OHIF User Preferences', () => {
|
|||||||
|
|
||||||
it('checks error message when invalid hotkey is inserted', function() {
|
it('checks error message when invalid hotkey is inserted', function() {
|
||||||
// Go go hotkeys tab
|
// Go go hotkeys tab
|
||||||
cy.get('@userPreferencesHotkeysTab').click();
|
cy.changePreferencesTab('@userPreferencesHotkeysTab');
|
||||||
|
|
||||||
// Set invalid hotkey for 'Rotate Right' function
|
// Set invalid hotkey for 'Rotate Right' function
|
||||||
cy.setNewHotkeyShortcutOnUserPreferencesModal('Rotate Right', '{ctrl}Z');
|
cy.setNewHotkeyShortcutOnUserPreferencesModal('Rotate Right', '{ctrl}Z');
|
||||||
|
|
||||||
// Check error message
|
// Check error message
|
||||||
cy.get('.HotKeysPreferences').within(() => {
|
cy.get('.HotkeysPreferences').within(() => {
|
||||||
cy.contains('Rotate Right') // label we're looking for
|
cy.contains('Rotate Right') // label we're looking for
|
||||||
.parent()
|
.parent()
|
||||||
.find('.errorMessage')
|
.find('.errorMessage')
|
||||||
@ -466,12 +453,12 @@ describe('OHIF User Preferences', () => {
|
|||||||
|
|
||||||
it('checks error message when only modifier keys are inserted', function() {
|
it('checks error message when only modifier keys are inserted', function() {
|
||||||
// Go go hotkeys tab
|
// Go go hotkeys tab
|
||||||
cy.get('@userPreferencesHotkeysTab').click();
|
cy.changePreferencesTab('@userPreferencesHotkeysTab');
|
||||||
|
|
||||||
// Set invalid modifier key: ctrl
|
// Set invalid modifier key: ctrl
|
||||||
cy.setNewHotkeyShortcutOnUserPreferencesModal('Zoom Out', '{ctrl}');
|
cy.setNewHotkeyShortcutOnUserPreferencesModal('Zoom Out', '{ctrl}');
|
||||||
// Check error message
|
// Check error message
|
||||||
cy.get('.HotKeysPreferences').within(() => {
|
cy.get('.HotkeysPreferences').within(() => {
|
||||||
cy.contains('Zoom Out') // label we're looking for
|
cy.contains('Zoom Out') // label we're looking for
|
||||||
.parent()
|
.parent()
|
||||||
.find('.errorMessage')
|
.find('.errorMessage')
|
||||||
@ -506,7 +493,7 @@ describe('OHIF User Preferences', () => {
|
|||||||
|
|
||||||
it('checks if user can cancel changes made on User Preferences Hotkeys tab', function() {
|
it('checks if user can cancel changes made on User Preferences Hotkeys tab', function() {
|
||||||
// Go go hotkeys tab
|
// Go go hotkeys tab
|
||||||
cy.get('@userPreferencesHotkeysTab').click();
|
cy.changePreferencesTab('@userPreferencesHotkeysTab');
|
||||||
|
|
||||||
// Set new hotkey for 'Rotate Right' function
|
// Set new hotkey for 'Rotate Right' function
|
||||||
cy.setNewHotkeyShortcutOnUserPreferencesModal(
|
cy.setNewHotkeyShortcutOnUserPreferencesModal(
|
||||||
@ -523,7 +510,7 @@ describe('OHIF User Preferences', () => {
|
|||||||
cy.openPreferences();
|
cy.openPreferences();
|
||||||
|
|
||||||
//Check that hotkey for 'Rotate Right' function was not changed
|
//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
|
cy.contains('Rotate Right') // label we're looking for
|
||||||
.parent()
|
.parent()
|
||||||
.find('input')
|
.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() {
|
it('checks if user can reset to default values on User Preferences Hotkeys tab', function() {
|
||||||
// Go go hotkeys tab
|
// Go go hotkeys tab
|
||||||
cy.get('@userPreferencesHotkeysTab').click();
|
cy.changePreferencesTab('@userPreferencesHotkeysTab');
|
||||||
|
|
||||||
// Set new hotkey for 'Rotate Right' function
|
// Set new hotkey for 'Rotate Right' function
|
||||||
cy.setNewHotkeyShortcutOnUserPreferencesModal(
|
cy.setNewHotkeyShortcutOnUserPreferencesModal(
|
||||||
@ -559,7 +546,7 @@ describe('OHIF User Preferences', () => {
|
|||||||
cy.openPreferences();
|
cy.openPreferences();
|
||||||
|
|
||||||
//Check that hotkey for 'Rotate Right' function was not changed
|
//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
|
cy.contains('Rotate Right') // label we're looking for
|
||||||
.parent()
|
.parent()
|
||||||
.find('input')
|
.find('input')
|
||||||
|
|||||||
@ -21,9 +21,8 @@ describe('Visual Regression - OHIF User Preferences', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('checks translation by selecting Spanish language', function() {
|
it('checks translation by selecting Spanish language', function() {
|
||||||
cy.get('@userPreferencesGeneralTab')
|
cy.changePreferencesTab('@userPreferencesGeneralTab');
|
||||||
.click()
|
cy.get('@userPreferencesGeneralTab').should('have.class', 'active');
|
||||||
.should('have.class', 'active');
|
|
||||||
|
|
||||||
// Language dropdown should be displayed
|
// Language dropdown should be displayed
|
||||||
cy.get('#language-select').should('be.visible');
|
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() {
|
it('checks translation by selecting Spanish language', function() {
|
||||||
cy.get('@userPreferencesGeneralTab')
|
cy.changePreferencesTab('@userPreferencesGeneralTab');
|
||||||
.click()
|
cy.get('@userPreferencesGeneralTab').should('have.class', 'active');
|
||||||
.should('have.class', 'active');
|
|
||||||
|
|
||||||
// Visual comparison
|
// Visual comparison
|
||||||
cy.percyCanvasSnapshot(
|
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() {
|
it('checks if user can restore to default the language selection and application will be in English', function() {
|
||||||
cy.get('@userPreferencesGeneralTab')
|
cy.changePreferencesTab('@userPreferencesGeneralTab');
|
||||||
.click()
|
cy.get('@userPreferencesGeneralTab').should('have.class', 'active');
|
||||||
.should('have.class', 'active');
|
|
||||||
|
|
||||||
// Set language to Spanish
|
// Set language to Spanish
|
||||||
cy.setLanguage('Spanish');
|
cy.setLanguage('Spanish');
|
||||||
@ -113,7 +110,7 @@ describe('Visual Regression - OHIF User Preferences', () => {
|
|||||||
cy.openPreferences();
|
cy.openPreferences();
|
||||||
|
|
||||||
// Go to general tab
|
// Go to general tab
|
||||||
cy.get('@userPreferencesGeneralTab').click();
|
cy.changePreferencesTab('@userPreferencesGeneralTab');
|
||||||
|
|
||||||
cy.get('@restoreBtn')
|
cy.get('@restoreBtn')
|
||||||
.scrollIntoView()
|
.scrollIntoView()
|
||||||
@ -139,9 +136,8 @@ describe('Visual Regression - OHIF User Preferences', () => {
|
|||||||
|
|
||||||
it('checks new hotkeys for "Next" and "Previous" Image on Viewport', function() {
|
it('checks new hotkeys for "Next" and "Previous" Image on Viewport', function() {
|
||||||
// Go go hotkeys tab
|
// Go go hotkeys tab
|
||||||
cy.get('@userPreferencesHotkeysTab')
|
cy.changePreferencesTab('@userPreferencesHotkeysTab');
|
||||||
.click()
|
cy.get('@userPreferencesHotkeysTab').should('have.class', 'active');
|
||||||
.should('have.class', 'active');
|
|
||||||
|
|
||||||
// Set new hotkey for 'Next Image Viewport' function
|
// Set new hotkey for 'Next Image Viewport' function
|
||||||
cy.setNewHotkeyShortcutOnUserPreferencesModal(
|
cy.setNewHotkeyShortcutOnUserPreferencesModal(
|
||||||
|
|||||||
@ -84,7 +84,12 @@ export function initPreferencesModalAliases() {
|
|||||||
cy.get('.OHIFModal').as('preferencesModal');
|
cy.get('.OHIFModal').as('preferencesModal');
|
||||||
cy.get('[data-cy="hotkeys"]').as('userPreferencesHotkeysTab');
|
cy.get('[data-cy="hotkeys"]').as('userPreferencesHotkeysTab');
|
||||||
cy.get('[data-cy="general"]').as('userPreferencesGeneralTab');
|
cy.get('[data-cy="general"]').as('userPreferencesGeneralTab');
|
||||||
cy.get('[data-cy="reset-default-btn"]').as('restoreBtn');
|
initPreferencesModalFooterBtnAliases();
|
||||||
cy.get('[data-cy="cancel-btn"]').as('cancelBtn');
|
}
|
||||||
cy.get('[data-cy="save-btn"]').as('saveBtn');
|
|
||||||
|
//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');
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import {
|
|||||||
initStudyListAliasesOnDesktop,
|
initStudyListAliasesOnDesktop,
|
||||||
initStudyListAliasesOnTablet,
|
initStudyListAliasesOnTablet,
|
||||||
initPreferencesModalAliases,
|
initPreferencesModalAliases,
|
||||||
|
initPreferencesModalFooterBtnAliases,
|
||||||
} from './aliases.js';
|
} from './aliases.js';
|
||||||
|
|
||||||
// ***********************************************
|
// ***********************************************
|
||||||
@ -169,13 +170,13 @@ Cypress.Commands.add(
|
|||||||
);
|
);
|
||||||
|
|
||||||
Cypress.Commands.add('expectMinimumThumbnails', (seriesToWait = 1) => {
|
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;
|
expect($itemList.length >= seriesToWait).to.be.true;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
//Command to wait DICOM image to load into the viewport
|
//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();
|
const loaded = cy.isPageLoaded();
|
||||||
|
|
||||||
if (loaded) {
|
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', () => {
|
Cypress.Commands.add('resetUserHoktkeyPreferences', () => {
|
||||||
// Open User Preferences modal
|
// Open User Preferences modal
|
||||||
cy.openPreferences();
|
cy.openPreferences();
|
||||||
@ -457,7 +464,7 @@ Cypress.Commands.add(
|
|||||||
(function_label, shortcut) => {
|
(function_label, shortcut) => {
|
||||||
// Within scopes all `.get` and `.contains` to within the matched elements
|
// Within scopes all `.get` and `.contains` to within the matched elements
|
||||||
// dom instead of checking from document
|
// dom instead of checking from document
|
||||||
cy.get('.HotKeysPreferences')
|
cy.get('.HotkeysPreferences')
|
||||||
.within(() => {
|
.within(() => {
|
||||||
cy.contains(function_label) // label we're looking for
|
cy.contains(function_label) // label we're looking for
|
||||||
.parent()
|
.parent()
|
||||||
@ -487,16 +494,16 @@ Cypress.Commands.add('setLanguage', (language, save = true) => {
|
|||||||
.click()
|
.click()
|
||||||
.should('have.class', 'active');
|
.should('have.class', 'active');
|
||||||
|
|
||||||
|
initPreferencesModalFooterBtnAliases();
|
||||||
|
|
||||||
// Language dropdown should be displayed
|
// Language dropdown should be displayed
|
||||||
cy.get('#language-select').should('be.visible');
|
cy.get('#language-select').should('be.visible');
|
||||||
|
|
||||||
// Select Language and Save/Cancel
|
// Select Language and Save/Cancel
|
||||||
cy.get('#language-select')
|
cy.get('#language-select').select(language);
|
||||||
.select(language)
|
|
||||||
.then(() => {
|
const toClick = save ? '@saveBtn' : '@cancelBtn';
|
||||||
const toClick = save ? '@saveBtn' : '@cancelBtn';
|
cy.get(toClick)
|
||||||
cy.get(toClick)
|
.scrollIntoView()
|
||||||
.scrollIntoView()
|
.click();
|
||||||
.click();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@ -25,12 +25,12 @@ window.config = {
|
|||||||
// ~ Global
|
// ~ Global
|
||||||
{
|
{
|
||||||
commandName: 'incrementActiveViewport',
|
commandName: 'incrementActiveViewport',
|
||||||
label: 'Next Image Viewport',
|
label: 'Next Viewport',
|
||||||
keys: ['right'],
|
keys: ['right'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
commandName: 'decrementActiveViewport',
|
commandName: 'decrementActiveViewport',
|
||||||
label: 'Previous Image Viewport',
|
label: 'Previous Viewport',
|
||||||
keys: ['left'],
|
keys: ['left'],
|
||||||
},
|
},
|
||||||
// Supported Keys: https://craig.is/killing/mice
|
// Supported Keys: https://craig.is/killing/mice
|
||||||
|
|||||||
@ -20,12 +20,12 @@ window.config = {
|
|||||||
hotkeys: [
|
hotkeys: [
|
||||||
{
|
{
|
||||||
commandName: 'incrementActiveViewport',
|
commandName: 'incrementActiveViewport',
|
||||||
label: 'Next Image Viewport',
|
label: 'Next Viewport',
|
||||||
keys: ['right'],
|
keys: ['right'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
commandName: 'decrementActiveViewport',
|
commandName: 'decrementActiveViewport',
|
||||||
label: 'Previous Image Viewport',
|
label: 'Previous Viewport',
|
||||||
keys: ['left'],
|
keys: ['left'],
|
||||||
},
|
},
|
||||||
{ commandName: 'rotateViewportCW', label: 'Rotate Right', keys: ['r'] },
|
{ commandName: 'rotateViewportCW', label: 'Rotate Right', keys: ['r'] },
|
||||||
|
|||||||
@ -20,12 +20,12 @@ window.config = {
|
|||||||
// ~ Global
|
// ~ Global
|
||||||
{
|
{
|
||||||
commandName: 'incrementActiveViewport',
|
commandName: 'incrementActiveViewport',
|
||||||
label: 'Next Image Viewport',
|
label: 'Next Viewport',
|
||||||
keys: ['right'],
|
keys: ['right'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
commandName: 'decrementActiveViewport',
|
commandName: 'decrementActiveViewport',
|
||||||
label: 'Previous Image Viewport',
|
label: 'Previous Viewport',
|
||||||
keys: ['left'],
|
keys: ['left'],
|
||||||
},
|
},
|
||||||
// Supported Keys: https://craig.is/killing/mice
|
// Supported Keys: https://craig.is/killing/mice
|
||||||
|
|||||||
@ -53,7 +53,6 @@ import store from './store';
|
|||||||
import WhiteLabellingContext from './context/WhiteLabellingContext';
|
import WhiteLabellingContext from './context/WhiteLabellingContext';
|
||||||
import UserManagerContext from './context/UserManagerContext';
|
import UserManagerContext from './context/UserManagerContext';
|
||||||
import AppContext from './context/AppContext';
|
import AppContext from './context/AppContext';
|
||||||
const { setUserPreferences } = reduxOHIF.actions;
|
|
||||||
|
|
||||||
/** ~~~~~~~~~~~~~ Application Setup */
|
/** ~~~~~~~~~~~~~ Application Setup */
|
||||||
const commandsManagerConfig = {
|
const commandsManagerConfig = {
|
||||||
@ -63,8 +62,8 @@ const commandsManagerConfig = {
|
|||||||
|
|
||||||
/** Managers */
|
/** Managers */
|
||||||
const commandsManager = new CommandsManager(commandsManagerConfig);
|
const commandsManager = new CommandsManager(commandsManagerConfig);
|
||||||
const hotkeysManager = new HotkeysManager(commandsManager);
|
|
||||||
const servicesManager = new ServicesManager();
|
const servicesManager = new ServicesManager();
|
||||||
|
const hotkeysManager = new HotkeysManager(commandsManager, servicesManager);
|
||||||
let extensionManager;
|
let extensionManager;
|
||||||
/** ~~~~~~~~~~~~~ End Application Setup */
|
/** ~~~~~~~~~~~~~ End Application Setup */
|
||||||
|
|
||||||
@ -116,7 +115,7 @@ class App extends Component {
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
servers,
|
servers,
|
||||||
hotkeys,
|
hotkeys: appConfigHotkeys,
|
||||||
cornerstoneExtensionConfig,
|
cornerstoneExtensionConfig,
|
||||||
extensions,
|
extensions,
|
||||||
oidc,
|
oidc,
|
||||||
@ -139,7 +138,7 @@ class App extends Component {
|
|||||||
* Must run after extension commands are registered
|
* Must run after extension commands are registered
|
||||||
* if there is no hotkeys from localStorage set up from config.
|
* if there is no hotkeys from localStorage set up from config.
|
||||||
*/
|
*/
|
||||||
_initHotkeys(hotkeys);
|
_initHotkeys(appConfigHotkeys);
|
||||||
_initServers(servers);
|
_initServers(servers);
|
||||||
initWebWorkers();
|
initWebWorkers();
|
||||||
}
|
}
|
||||||
@ -263,30 +262,27 @@ function _initExtensions(extensions, cornerstoneExtensionConfig, appConfig) {
|
|||||||
extensionManager.registerExtensions(mergedExtensions);
|
extensionManager.registerExtensions(mergedExtensions);
|
||||||
}
|
}
|
||||||
|
|
||||||
function _initHotkeys(hotkeys) {
|
/**
|
||||||
const { hotkeyDefinitions = {} } = store.getState().preferences || {};
|
*
|
||||||
let updateStore = false;
|
* @param {Object} appConfigHotkeys - Default hotkeys, as defined by app config
|
||||||
let hotkeysToUse = hotkeyDefinitions;
|
*/
|
||||||
|
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) {
|
// TODO: hotkeysManager.isValidDefinitionObject(/* */)
|
||||||
hotkeysToUse = hotkeys;
|
const hasUserPreferences =
|
||||||
updateStore = true;
|
userPreferredHotkeys && Object.keys(userPreferredHotkeys).length > 0;
|
||||||
|
if (hasUserPreferences) {
|
||||||
|
hotkeysManager.setHotkeys(userPreferredHotkeys);
|
||||||
|
} else {
|
||||||
|
hotkeysManager.setHotkeys(appConfigHotkeys);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hotkeysToUse) {
|
hotkeysManager.setDefaultHotKeys(appConfigHotkeys);
|
||||||
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 })
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function _initServers(servers) {
|
function _initServers(servers) {
|
||||||
|
|||||||
@ -1,10 +1,12 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { Link, withRouter } from 'react-router-dom';
|
import { Link, withRouter } from 'react-router-dom';
|
||||||
import { withTranslation } from 'react-i18next';
|
import { withTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import ConnectedUserPreferencesForm from '../../connectedComponents/ConnectedUserPreferencesForm';
|
|
||||||
import { Dropdown, AboutContent, withModal } from '@ohif/ui';
|
import { Dropdown, AboutContent, withModal } from '@ohif/ui';
|
||||||
|
|
||||||
|
import { UserPreferences } from './../UserPreferences';
|
||||||
import OHIFLogo from '../OHIFLogo/OHIFLogo.js';
|
import OHIFLogo from '../OHIFLogo/OHIFLogo.js';
|
||||||
import './Header.css';
|
import './Header.css';
|
||||||
|
|
||||||
@ -42,7 +44,7 @@ function Header(props) {
|
|||||||
},
|
},
|
||||||
onClick: () =>
|
onClick: () =>
|
||||||
show({
|
show({
|
||||||
content: ConnectedUserPreferencesForm,
|
content: UserPreferences,
|
||||||
title: t('User Preferences'),
|
title: t('User Preferences'),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
|||||||
@ -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 (
|
||||||
|
<React.Fragment>
|
||||||
|
<div className="GeneralPreferences">
|
||||||
|
<div className="language">
|
||||||
|
<label htmlFor="language-select" className="languageLabel">
|
||||||
|
Language
|
||||||
|
</label>
|
||||||
|
<LanguageSwitcher
|
||||||
|
language={language}
|
||||||
|
onLanguageChange={setLanguage}
|
||||||
|
languages={availableLanguages}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<TabFooter
|
||||||
|
onResetPreferences={onResetPreferences}
|
||||||
|
onSave={onSave}
|
||||||
|
onCancel={onClose}
|
||||||
|
hasErrors={hasErrors}
|
||||||
|
t={t}
|
||||||
|
/>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
GeneralPreferences.propTypes = {
|
||||||
|
onClose: PropTypes.func,
|
||||||
|
};
|
||||||
|
|
||||||
|
export { GeneralPreferences };
|
||||||
@ -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;
|
||||||
@ -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 (
|
||||||
|
<React.Fragment>
|
||||||
|
<div className="HotkeysPreferences">
|
||||||
|
{hasHotkeys ? (
|
||||||
|
<div className="hotkeyTable">
|
||||||
|
{splitedHotkeys.map((hotkeys, index) => {
|
||||||
|
return (
|
||||||
|
<div className="hotkeyColumn" key={index}>
|
||||||
|
<div className="hotkeyHeader">
|
||||||
|
<div className="headerItemText text-right">Function</div>
|
||||||
|
<div className="headerItemText text-center">Shortcut</div>
|
||||||
|
</div>
|
||||||
|
{hotkeys.map(hotkey => {
|
||||||
|
const commandName = hotkey[0];
|
||||||
|
const hotkeyDefinition = hotkey[1];
|
||||||
|
const { keys, label } = hotkeyDefinition;
|
||||||
|
const errorMessage = state.errors[hotkey[0]];
|
||||||
|
const handleChange = keys => {
|
||||||
|
onHotkeyChanged(commandName, hotkeyDefinition, keys);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={commandName} className="hotkeyRow">
|
||||||
|
<div className="hotkeyLabel">{label}</div>
|
||||||
|
<div
|
||||||
|
data-key="defaultTool"
|
||||||
|
className={classnames(
|
||||||
|
'wrapperHotkeyInput',
|
||||||
|
errorMessage ? 'stateError' : ''
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<HotkeyField
|
||||||
|
keys={keys}
|
||||||
|
modifier_keys={MODIFIER_KEYS}
|
||||||
|
handleChange={handleChange}
|
||||||
|
classNames={'hotkeyInput'}
|
||||||
|
></HotkeyField>
|
||||||
|
<span className="errorMessage">{errorMessage}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
'Hotkeys definitions is empty'
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<TabFooter
|
||||||
|
onResetPreferences={onResetPreferences}
|
||||||
|
onSave={onSave}
|
||||||
|
onCancel={onClose}
|
||||||
|
hasErrors={hasErrors}
|
||||||
|
t={t}
|
||||||
|
/>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
HotkeysPreferences.propTypes = {
|
||||||
|
onClose: PropTypes.func,
|
||||||
|
};
|
||||||
|
|
||||||
|
export { HotkeysPreferences };
|
||||||
@ -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)
|
||||||
@ -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 <TabComponents tabs={tabs} customProps={customProps} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
UserPreferences.propTypes = {
|
||||||
|
hide: PropTypes.func,
|
||||||
|
};
|
||||||
|
|
||||||
|
export { UserPreferences };
|
||||||
@ -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 (
|
||||||
|
<React.Fragment>
|
||||||
|
<div className="">Component content: {name}</div>
|
||||||
|
<div className="">TDB!</div>
|
||||||
|
<TabFooter
|
||||||
|
onResetPreferences={onResetPreferences}
|
||||||
|
onSave={onSave}
|
||||||
|
onCancel={onClose}
|
||||||
|
hasErrors={hasErrors}
|
||||||
|
t={t}
|
||||||
|
/>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
WindowLevelPreferences.propTypes = {
|
||||||
|
onClose: PropTypes.func,
|
||||||
|
};
|
||||||
|
|
||||||
|
export { WindowLevelPreferences };
|
||||||
@ -2,7 +2,9 @@ const range = (start, end) => {
|
|||||||
return new Array(end - start).fill().map((d, i) => i + start);
|
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'],
|
alt: ['space'],
|
||||||
shift: [],
|
shift: [],
|
||||||
@ -34,18 +36,7 @@ export const disallowedCombinations = {
|
|||||||
'ctrl+shift': ['q', 'w', 'r', 't', 'p', 'a', 'h', 'v', 'b', 'n'],
|
'ctrl+shift': ['q', 'w', 'r', 't', 'p', 'a', 'h', 'v', 'b', 'n'],
|
||||||
};
|
};
|
||||||
|
|
||||||
export const allowedKeys = [
|
export const SPECIAL_KEYS = {
|
||||||
...[8, 13, 27, 32, 46], // backspace, enter, escape, space, delete
|
|
||||||
...[12, 106, 107, 109, 110, 111], // Numpad keys
|
|
||||||
...range(218, 220), // [\]
|
|
||||||
...range(185, 190), // ;=,-./
|
|
||||||
...range(111, 131), // F1-F19
|
|
||||||
...range(32, 41), // arrow keys, home/end, pg dn/up
|
|
||||||
...range(47, 58), // 0-9
|
|
||||||
...range(64, 91), // A-Z
|
|
||||||
];
|
|
||||||
|
|
||||||
export const specialKeys = {
|
|
||||||
8: 'backspace',
|
8: 'backspace',
|
||||||
9: 'tab',
|
9: 'tab',
|
||||||
13: 'return',
|
13: 'return',
|
||||||
@ -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 };
|
||||||
1
platform/viewer/src/components/UserPreferences/index.js
Normal file
1
platform/viewer/src/components/UserPreferences/index.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { UserPreferences } from './UserPreferences';
|
||||||
@ -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;
|
|
||||||
Loading…
Reference in New Issue
Block a user