fix: User Preferences Issues (#1207)
* fix: study list translations * Don't render until translations are ready * Try to wait for translations to load * Use null to render "nothing" * Try toggling useSuspense off up a layer * logging * Remove useSuspense false flags * DO NOT OPEN A PR WITH IT. Experimental changes only. Try to solve issues with useTranslation hooks #Please enter the commit message for your changes. Lines starting * Remove unecessary changes * feat: 🎸 useMedia hook to not use one more prop for upd state vl * docs: Add license scan report and status (#1161) Signed-off-by: fossabot <badges@fossa.io> * fix: 🐛 Fix for JS breaking on header (#1164) * feat: 🎸 Code review and refact Revised code based on PRs(variable alias,...). Changed hook for useMedia. Now, it provides two hooks: one to get displayMediaSize and other to get Entity(component, objects...) based on displayMediaSize. Implemented a similar solution for state manager to store mediaQueryList(s) and displaySize for app. * feat: 🎸 Fine tunning on mediaQuery value, fixed issue about it * fix: 🐛 Fixed issue and refactoring Fixed js exception and also refactoring userPreferences components to functional component * fix: 🐛 Code review. Localstorage fix minor bugs * fix: 🐛 Ensure hotkey lower case always * fix: translation switcher * chore(release): publish [skip ci] - @ohif/extension-vtk@0.53.6 - @ohif/ui@0.62.1 - @ohif/viewer@2.8.2 * Add new modal service * Change serviceManager prop to servicesManager * CR Update: fix casing and add required proptypes to providers * CR Update: Improve ohifmodal proptypes * CR Update: Fix typo in extensionmanager * CR Update: add default props to service and check service in provider * Refactor modal provider to better use its own state * ci: don't build our master branch (#1177) * ci: don't build our master branch * Add netlify-cli as a dev dependency * ci: trying a sheltered merge flow for promotions * Use modal instead of modal context * Ci/promotable builds (#1179) * ci: don't build our master branch * Add netlify-cli as a dev dependency * ci: trying a sheltered merge flow for promotions * ci: try building with QUICK_BUILD flag * Change modal children order * Ci/promotable builds (#1180) * ci: don't build our master branch * Add netlify-cli as a dev dependency * ci: trying a sheltered merge flow for promotions * ci: try building with QUICK_BUILD flag * Try using ~/repo prefix in command * Ci/promotable builds (#1181) * ci: don't build our master branch * Add netlify-cli as a dev dependency * ci: trying a sheltered merge flow for promotions * ci: try building with QUICK_BUILD flag * Try using ~/repo prefix in command * ci: make sure netlify command is available * Ci/promotable builds (#1182) * ci: don't build our master branch * Add netlify-cli as a dev dependency * ci: trying a sheltered merge flow for promotions * ci: try building with QUICK_BUILD flag * Try using ~/repo prefix in command * ci: make sure netlify command is available * ci: use sudo for global command * Fix OHIFModal proptypes * Ci/promotable builds (#1183) * ci: don't build our master branch * Add netlify-cli as a dev dependency * ci: trying a sheltered merge flow for promotions * ci: try building with QUICK_BUILD flag * Try using ~/repo prefix in command * ci: make sure netlify command is available * ci: use sudo for global command * Inline personal access token w/ env var * Ci/promotable builds (#1184) * ci: don't build our master branch * Add netlify-cli as a dev dependency * ci: trying a sheltered merge flow for promotions * ci: try building with QUICK_BUILD flag * Try using ~/repo prefix in command * ci: make sure netlify command is available * ci: use sudo for global command * Inline personal access token w/ env var * ci: workaround for sudo limitations * ci: restore release workflow (#1185) * ci: don't build our master branch * Add netlify-cli as a dev dependency * ci: trying a sheltered merge flow for promotions * ci: try building with QUICK_BUILD flag * Try using ~/repo prefix in command * ci: make sure netlify command is available * ci: use sudo for global command * Inline personal access token w/ env var * ci: workaround for sudo limitations * ci: restore release workflow * Pass services to each module, improve tests * Add servicesManager test and registerServices method * Fix key warning of snackbar elements * Remove netlify-cli; we';ll install this on CI server * Update staging and prod netlify site IDs * Clean up NPM_PUBLISH step * Clean up DOCS_PUBLISH step * Clean up Deploy workflow * Custom executor to override cypress config * Spacing * Use an existing docker hub image * Switch to npx instead of digging into npm bin location * Remove e2e test before prod deploy * Add workflow images * docs: continous integrationn * Add default props to modal * chore(release): publish [skip ci] - @ohif/extension-cornerstone@1.5.1 - @ohif/extension-vtk@0.53.7 - @ohif/core@1.9.1 - @ohif/ui@0.62.2 - @ohif/viewer@2.8.3 * chore(release): publish [skip ci] - @ohif/extension-vtk@0.53.8 - @ohif/core@1.10.0 - @ohif/ui@0.62.3 - @ohif/viewer@2.8.4 * ci: Redirect site traffic to index.html if file is not resolved * ci: fix typo * fix: 🐛 Code review. Remove 'global state' for displaySize * fix: 🐛 Code review. Ref back to useMedia and pass value down Code review. Ref back to useMedia and pass value down (components) instead of creating a specialized hook to tied any component on it * fix: translation switcher * fix * cleanup * add missed translation * fix: 🐛 Code review * fix: 🐛 Fix unit tests * fix: 🐛 Fix unit test * fix: 🐛 Code merge solve conflicts. Missing files changes * Merge from master. Missing files changes * feat: 🎸 Code review. Fix issues with DatePicker Fixed at least the minimum issue with datePicker and update some content on every translation changed * fix: 🐛 Code review. Style fix on modal * fixes in general preferences after merge * fix * export default i18n language * translation fixes * increase snackbar zindex * initialize language * WIP propagate props<>state fixes * fix state propagation * remove hotkey error when reset default * fix datePicker * i18n updates * fix i18n strings * fix reset default -- set right input value * fix default language * fix e2e for user preferences * small fix datePicker props * remove i18n from window -- debugging purposes only * lint fixes
This commit is contained in:
parent
fd1a9e2abe
commit
1df21a9e07
@ -1,4 +1,3 @@
|
|||||||
import cloneDeep from 'lodash.clonedeep';
|
|
||||||
import hotkeys from './hotkeys';
|
import hotkeys from './hotkeys';
|
||||||
import log from './../log.js';
|
import log from './../log.js';
|
||||||
|
|
||||||
@ -44,20 +43,68 @@ export class HotkeysManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Registers a list of hotkeydefinitions. Optionally, sets the
|
* Registers a list of hotkeydefinitions.
|
||||||
* default hotkey bindings for all provided definitions. These
|
*
|
||||||
|
* @param {HotkeyDefinition[] | Object} hotkeyDefinitions Contains hotkeys definitions
|
||||||
|
*/
|
||||||
|
setHotkeys(hotkeyDefinitions) {
|
||||||
|
const definitions = Array.isArray(hotkeyDefinitions)
|
||||||
|
? [...hotkeyDefinitions]
|
||||||
|
: this._parseToArrayLike(hotkeyDefinitions);
|
||||||
|
|
||||||
|
definitions.forEach(definition => this.registerHotkeys(definition));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set default hotkey bindings. These
|
||||||
* values are used in `this.restoreDefaultBindings`.
|
* values are used in `this.restoreDefaultBindings`.
|
||||||
*
|
*
|
||||||
* @param {HotkeyDefinition[]} hotkeyDefinitions
|
* @param {HotkeyDefinition[] | Object} hotkeyDefinitions Contains hotkeys definitions
|
||||||
* @param {Boolean} [isDefaultDefinitions]
|
|
||||||
*/
|
*/
|
||||||
setHotkeys(hotkeyDefinitions, isDefaultDefinitions = false) {
|
setDefaultHotKeys(hotkeyDefinitions) {
|
||||||
const definitions = cloneDeep(hotkeyDefinitions);
|
const definitions = Array.isArray(hotkeyDefinitions)
|
||||||
definitions.forEach(definition => this.registerHotkeys(definition));
|
? [...hotkeyDefinitions]
|
||||||
|
: this._parseToArrayLike(hotkeyDefinitions);
|
||||||
|
|
||||||
if (isDefaultDefinitions) {
|
this.hotkeyDefaults = definitions;
|
||||||
this.hotkeyDefaults = definitions;
|
}
|
||||||
}
|
|
||||||
|
/**
|
||||||
|
* It parses given object containing hotkeyDefinition to array like.
|
||||||
|
* Each property of given object will be mapped to an object of an array. And its property name will be the value of a property named as commandName
|
||||||
|
*
|
||||||
|
* @param {HotkeyDefinition[] | Object} hotkeyDefinitions Contains hotkeys definitions
|
||||||
|
* @returns {HotkeyDefinition[]}
|
||||||
|
*/
|
||||||
|
_parseToArrayLike(hotkeyDefinitionsObj) {
|
||||||
|
const copy = { ...hotkeyDefinitionsObj };
|
||||||
|
return Object.entries(copy).map(entryValue =>
|
||||||
|
this._parseToHotKeyObj(entryValue[0], entryValue[1])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return HotkeyDefinition object like based on given property name and property value
|
||||||
|
* @param {string} propertyName property name of hotkey definition object
|
||||||
|
* @param {object} propertyValue property value of hotkey definition object
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
*
|
||||||
|
* const hotKeyObj = {hotKeyDefA: {keys:[],....}}
|
||||||
|
*
|
||||||
|
* const parsed = _parseToHotKeyObj(Object.keys(hotKeyDefA)[0], hotKeyObj[hotKeyDefA]);
|
||||||
|
* {
|
||||||
|
* commandName: hotKeyDefA,
|
||||||
|
* keys: [],
|
||||||
|
* ....
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
_parseToHotKeyObj(propertyName, propertyValue) {
|
||||||
|
return {
|
||||||
|
commandName: propertyName,
|
||||||
|
...propertyValue,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -94,18 +94,20 @@ describe('HotkeysManager', () => {
|
|||||||
expect(firstCallArgs).toEqual(hotkeyDefinitions[0]);
|
expect(firstCallArgs).toEqual(hotkeyDefinitions[0]);
|
||||||
expect(secondCallArgs).toEqual(hotkeyDefinitions[1]);
|
expect(secondCallArgs).toEqual(hotkeyDefinitions[1]);
|
||||||
});
|
});
|
||||||
it('does not set this.hotkeyDefaults by default', () => {
|
it('does not set this.hotkeyDefaults when calling setHotKeys', () => {
|
||||||
const hotkeyDefinitions = [{ commandName: 'dance', keys: '+' }];
|
const hotkeyDefinitions = [{ commandName: 'dance', keys: '+' }];
|
||||||
|
|
||||||
hotkeysManager.setHotkeys(hotkeyDefinitions);
|
hotkeysManager.setHotkeys(hotkeyDefinitions);
|
||||||
|
|
||||||
expect(hotkeysManager.hotkeyDefaults).toEqual([]);
|
expect(hotkeysManager.hotkeyDefaults).toEqual([]);
|
||||||
});
|
});
|
||||||
it('sets this.hotkeyDefaults when isDefaultDefinitions is true', () => {
|
});
|
||||||
const hotkeyDefinitions = [{ commandName: 'dance', keys: '+' }];
|
|
||||||
const isDefaultDefinitions = true;
|
|
||||||
|
|
||||||
hotkeysManager.setHotkeys(hotkeyDefinitions, isDefaultDefinitions);
|
describe('setDefaultHotKeys()', () => {
|
||||||
|
it('it sets default hotkeys', () => {
|
||||||
|
const hotkeyDefinitions = [{ commandName: 'dance', keys: '+' }];
|
||||||
|
|
||||||
|
hotkeysManager.setDefaultHotKeys(hotkeyDefinitions);
|
||||||
|
|
||||||
expect(hotkeysManager.hotkeyDefaults).toEqual(hotkeyDefinitions);
|
expect(hotkeysManager.hotkeyDefaults).toEqual(hotkeyDefinitions);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -11,12 +11,15 @@ const defaultState = {
|
|||||||
// order, description, window (int), level (int)
|
// order, description, window (int), level (int)
|
||||||
// 0: { description: 'Soft tissue', window: '', level: '' },
|
// 0: { description: 'Soft tissue', window: '', level: '' },
|
||||||
},
|
},
|
||||||
|
generalPreferences: {
|
||||||
|
// language: 'en-US'
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const preferences = (state, action) => {
|
const preferences = (state, action) => {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case 'SET_USER_PREFERENCES': {
|
case 'SET_USER_PREFERENCES': {
|
||||||
const newState = action.state ? action.state : cloneDeep(defaultState);
|
const newState = action.state || cloneDeep(defaultState);
|
||||||
|
|
||||||
return Object.assign({}, state, newState);
|
return Object.assign({}, state, newState);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -42,6 +42,7 @@ const locizeOptions = {
|
|||||||
|
|
||||||
const envUseLocize = !!config.USE_LOCIZE;
|
const envUseLocize = !!config.USE_LOCIZE;
|
||||||
const envApiKeyAvailable = !!config.LOCIZE_API_KEY;
|
const envApiKeyAvailable = !!config.LOCIZE_API_KEY;
|
||||||
|
const DEFAULT_LANGUAGE = 'en-US';
|
||||||
|
|
||||||
function initI18n(
|
function initI18n(
|
||||||
detection = detectionOptions,
|
detection = detectionOptions,
|
||||||
@ -74,7 +75,7 @@ function initI18n(
|
|||||||
// init i18next
|
// init i18next
|
||||||
// for all options read: https://www.i18next.com/overview/configuration-options
|
// for all options read: https://www.i18next.com/overview/configuration-options
|
||||||
.init({
|
.init({
|
||||||
fallbackLng: 'en-US',
|
fallbackLng: DEFAULT_LANGUAGE,
|
||||||
saveMissing: apiKeyAvailable,
|
saveMissing: apiKeyAvailable,
|
||||||
debug: debugMode,
|
debug: debugMode,
|
||||||
keySeparator: false,
|
keySeparator: false,
|
||||||
@ -111,7 +112,7 @@ function initI18n(
|
|||||||
// init i18next
|
// init i18next
|
||||||
// for all options read: https://www.i18next.com/overview/configuration-options
|
// for all options read: https://www.i18next.com/overview/configuration-options
|
||||||
.init({
|
.init({
|
||||||
fallbackLng: 'en-US',
|
fallbackLng: DEFAULT_LANGUAGE,
|
||||||
resources: locales,
|
resources: locales,
|
||||||
debug: debugMode,
|
debug: debugMode,
|
||||||
keySeparator: false,
|
keySeparator: false,
|
||||||
@ -136,5 +137,6 @@ customDebug(`version ${pkg.version} loaded.`, 'info');
|
|||||||
i18n.initializing = initI18n();
|
i18n.initializing = initI18n();
|
||||||
i18n.initI18n = initI18n;
|
i18n.initI18n = initI18n;
|
||||||
i18n.addLocales = addLocales;
|
i18n.addLocales = addLocales;
|
||||||
|
i18n.defaultLanguage = DEFAULT_LANGUAGE;
|
||||||
|
|
||||||
export default i18n;
|
export default i18n;
|
||||||
|
|||||||
3
platform/i18n/src/locales/ar/UserPreferencesModal.json
Normal file
3
platform/i18n/src/locales/ar/UserPreferencesModal.json
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"No hotkeys found": "Nenhuma tecla de atalho está configurada para este aplicativo. As teclas de atalho podem ser configuradas no arquivo app-config.js do aplicativo."
|
||||||
|
}
|
||||||
7
platform/i18n/src/locales/ar/index.js
Normal file
7
platform/i18n/src/locales/ar/index.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import UserPreferencesModal from "./UserPreferencesModal.json";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
'ar': {
|
||||||
|
UserPreferencesModal,
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -1,6 +1,9 @@
|
|||||||
{
|
{
|
||||||
"Cancel": "$t(Buttons:Cancel)",
|
"Cancel": "$t(Buttons:Cancel)",
|
||||||
|
"No hotkeys found": "No hotkeys are configured for this application. Hotkeys can be configured in the application's app-config.js file.",
|
||||||
"Reset to Defaults": "$t(Buttons:Reset to Defaults)",
|
"Reset to Defaults": "$t(Buttons:Reset to Defaults)",
|
||||||
|
"ResetDefaultMessage": "Preferences successfully reset to default. <br /> You must <strong>Save</strong> to perform this action.",
|
||||||
"Save": "$t(Buttons:Save)",
|
"Save": "$t(Buttons:Save)",
|
||||||
|
"SaveMessage": "Preferences saved",
|
||||||
"User Preferences": "User Preferences"
|
"User Preferences": "User Preferences"
|
||||||
}
|
}
|
||||||
@ -1,3 +1,4 @@
|
|||||||
|
import ar from './ar/';
|
||||||
import en_US from './en-US/';
|
import en_US from './en-US/';
|
||||||
import es from './es/';
|
import es from './es/';
|
||||||
import ja_JP from './ja-JP/';
|
import ja_JP from './ja-JP/';
|
||||||
@ -7,6 +8,7 @@ import vi from './vi/';
|
|||||||
import zh from './zh/';
|
import zh from './zh/';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
...ar,
|
||||||
...en_US,
|
...en_US,
|
||||||
...es,
|
...es,
|
||||||
...ja_JP,
|
...ja_JP,
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
{
|
{
|
||||||
"Cancel": "Cancelar",
|
"Cancel": "Cancelar",
|
||||||
"Reset to Defaults": "Restaurar Default",
|
"Reset to Defaults": "Restaurar Default",
|
||||||
|
"ResetDefaultMessage": "Preferências resetadas com sucesso. <br /> Você deve <strong>Salvar</strong> para que essa ação seja realizada.",
|
||||||
"Save": "Salvar",
|
"Save": "Salvar",
|
||||||
|
"SaveMessage": "Preferências salvas",
|
||||||
"User Preferences": "Preferências do Usuário"
|
"User Preferences": "Preferências do Usuário"
|
||||||
}
|
}
|
||||||
@ -1,14 +1,12 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React from 'react';
|
||||||
import i18n from '@ohif/i18n';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import './LanguageSwitcher.styl';
|
import './LanguageSwitcher.styl';
|
||||||
import { withTranslation } from '../../contextProviders';
|
import { withTranslation } from '../../contextProviders';
|
||||||
|
|
||||||
const LanguageSwitcher = () => {
|
const LanguageSwitcher = ({ language, onLanguageChange }) => {
|
||||||
const getCurrentLanguage = (language = i18n.language) =>
|
const parseLanguage = lang => lang.split('-')[0];
|
||||||
language.split('-')[0];
|
|
||||||
|
|
||||||
const [currentLanguage, setCurrentLanguage] = useState(getCurrentLanguage());
|
|
||||||
const languages = [
|
const languages = [
|
||||||
// TODO: list of available languages should come from i18n.options.resources
|
// TODO: list of available languages should come from i18n.options.resources
|
||||||
{
|
{
|
||||||
@ -21,46 +19,31 @@ const LanguageSwitcher = () => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const onChange = () => {
|
const onChange = event => {
|
||||||
const { value } = event.target;
|
const { value } = event.target;
|
||||||
const language = getCurrentLanguage(value);
|
onLanguageChange(parseLanguage(value));
|
||||||
setCurrentLanguage(language);
|
|
||||||
|
|
||||||
i18n.init({
|
|
||||||
fallbackLng: language,
|
|
||||||
lng: language,
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
let mounted = true;
|
|
||||||
|
|
||||||
i18n.on('languageChanged', () => {
|
|
||||||
if (mounted) {
|
|
||||||
setCurrentLanguage(getCurrentLanguage());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
mounted = false;
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<select
|
<select
|
||||||
name="language-select"
|
name="language-select"
|
||||||
id="language-select"
|
id="language-select"
|
||||||
className="language-select"
|
className="language-select"
|
||||||
value={currentLanguage}
|
value={parseLanguage(language)}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
>
|
>
|
||||||
{languages.map(language => (
|
{languages.map(lng => (
|
||||||
<option key={language.value} value={language.value}>
|
<option key={lng.value} value={lng.value}>
|
||||||
{language.label}
|
{lng.label}
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
LanguageSwitcher.propTypes = {
|
||||||
|
language: PropTypes.string.isRequired,
|
||||||
|
onLanguageChange: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
export default withTranslation('UserPreferencesModal')(LanguageSwitcher);
|
export default withTranslation('UserPreferencesModal')(LanguageSwitcher);
|
||||||
|
|||||||
@ -131,7 +131,6 @@ CustomDateRangePicker.propTypes = {
|
|||||||
end: PropTypes.required,
|
end: PropTypes.required,
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
autoFocus: PropTypes.bool.isRequired,
|
|
||||||
onDatesChange: PropTypes.func.isRequired,
|
onDatesChange: PropTypes.func.isRequired,
|
||||||
startDate: PropTypes.instanceOf(Date),
|
startDate: PropTypes.instanceOf(Date),
|
||||||
endDate: PropTypes.instanceOf(Date),
|
endDate: PropTypes.instanceOf(Date),
|
||||||
|
|||||||
@ -81,7 +81,7 @@ function StudyList(props) {
|
|||||||
|
|
||||||
const mediumTableMeta = [
|
const mediumTableMeta = [
|
||||||
{
|
{
|
||||||
displayText: `${t('Patient')} / ${t('MRN')}`,
|
displayText: `${t('PatientName')} / ${t('MRN')}`,
|
||||||
fieldName: 'patientNameOrId',
|
fieldName: 'patientNameOrId',
|
||||||
inputType: 'text',
|
inputType: 'text',
|
||||||
size: 250,
|
size: 250,
|
||||||
@ -187,7 +187,6 @@ function StudyList(props) {
|
|||||||
studyDate={study.studyDate}
|
studyDate={study.studyDate}
|
||||||
studyDescription={study.studyDescription || ''}
|
studyDescription={study.studyDescription || ''}
|
||||||
studyInstanceUid={study.studyInstanceUid}
|
studyInstanceUid={study.studyInstanceUid}
|
||||||
t={t}
|
|
||||||
displaySize={displaySize}
|
displaySize={displaySize}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
@ -239,10 +238,11 @@ function TableRow(props) {
|
|||||||
studyDescription,
|
studyDescription,
|
||||||
studyInstanceUid,
|
studyInstanceUid,
|
||||||
onClick: handleClick,
|
onClick: handleClick,
|
||||||
t,
|
|
||||||
displaySize,
|
displaySize,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
|
const { t } = useTranslation('StudyList');
|
||||||
|
|
||||||
const largeRowTemplate = (
|
const largeRowTemplate = (
|
||||||
<tr
|
<tr
|
||||||
onClick={() => handleClick(studyInstanceUid)}
|
onClick={() => handleClick(studyInstanceUid)}
|
||||||
|
|||||||
@ -103,15 +103,14 @@ function TableSearchFilter(props) {
|
|||||||
// https://github.com/airbnb/react-dates
|
// https://github.com/airbnb/react-dates
|
||||||
<CustomDateRangePicker
|
<CustomDateRangePicker
|
||||||
// Required
|
// Required
|
||||||
startDate={getDateEntry(studyDateTo, defaultStartDate)}
|
startDate={getDateEntry(studyDateFrom, defaultStartDate)}
|
||||||
startDateId="start-date"
|
startDateId="start-date"
|
||||||
endDate={getDateEntry(studyDateFrom, defaultEndDate)}
|
endDate={getDateEntry(studyDateTo, defaultEndDate)}
|
||||||
endDateId="end-date"
|
endDateId="end-date"
|
||||||
autoFocus={false}
|
|
||||||
// TODO: We need a dynamic way to determine which fields values to update
|
// TODO: We need a dynamic way to determine which fields values to update
|
||||||
onDatesChange={({ startDate, endDate, preset = false }) => {
|
onDatesChange={({ startDate, endDate, preset = false }) => {
|
||||||
onValueChange('studyDateTo', startDate);
|
onValueChange('studyDateFrom', startDate);
|
||||||
onValueChange('studyDateFrom', endDate);
|
onValueChange('studyDateTo', endDate);
|
||||||
}}
|
}}
|
||||||
focusedInput={focusedInput}
|
focusedInput={focusedInput}
|
||||||
onFocusChange={updatedVal => setFocusedInput(updatedVal)}
|
onFocusChange={updatedVal => setFocusedInput(updatedVal)}
|
||||||
|
|||||||
@ -1,17 +1,57 @@
|
|||||||
import React, { Component } from 'react';
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import LanguageSwitcher from '../languageSwitcher';
|
import LanguageSwitcher from '../languageSwitcher';
|
||||||
|
import i18n from '@ohif/i18n';
|
||||||
|
|
||||||
export class GeneralPreferences extends Component {
|
/**
|
||||||
render() {
|
* General Preferences tab
|
||||||
return (
|
*/
|
||||||
<div className="general-preferences-wrapper">
|
|
||||||
<div className="col-sm-3">
|
/**
|
||||||
<label htmlFor="language-select" className="p-r-1">
|
* General Preferences tab
|
||||||
Language
|
* It renders the General Preferences content
|
||||||
</label>
|
*
|
||||||
<LanguageSwitcher />
|
* It stores current state and whenever it changes, component messages parent of new value (through function callback)
|
||||||
</div>
|
* @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>
|
||||||
);
|
</div>
|
||||||
}
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
GeneralPreferences.propTypes = {
|
||||||
|
generalPreferences: PropTypes.any,
|
||||||
|
name: PropTypes.string,
|
||||||
|
onTabStateChanged: PropTypes.func,
|
||||||
|
onTabErrorChanged: PropTypes.func,
|
||||||
|
};
|
||||||
|
|
||||||
|
export { GeneralPreferences };
|
||||||
|
|||||||
@ -1,80 +1,238 @@
|
|||||||
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import './HotKeysPreferences.styl';
|
import './HotKeysPreferences.styl';
|
||||||
import React, { Component } from 'react';
|
|
||||||
import {
|
import {
|
||||||
allowedKeys,
|
allowedKeys,
|
||||||
disallowedCombinations,
|
disallowedCombinations,
|
||||||
specialKeys,
|
specialKeys,
|
||||||
} from './hotKeysConfig.js';
|
} from './hotKeysConfig.js';
|
||||||
|
|
||||||
import PropTypes from 'prop-types';
|
import isEqual from 'lodash.isequal';
|
||||||
|
|
||||||
export class HotKeysPreferences extends Component {
|
const getKeysPressedArray = keyDownEvent => {
|
||||||
static propTypes = {
|
const keysPressedArray = [];
|
||||||
hotkeyDefinitions: PropTypes.arrayOf(
|
const { ctrlKey, altKey, shiftKey } = keyDownEvent;
|
||||||
PropTypes.shape({
|
|
||||||
commandName: PropTypes.string,
|
if (ctrlKey && !altKey) {
|
||||||
keys: PropTypes.arrayOf(PropTypes.string),
|
keysPressedArray.push('ctrl');
|
||||||
label: PropTypes.string,
|
}
|
||||||
})
|
|
||||||
).isRequired,
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(props) {
|
const emptyValidator = ({ inputValue }) => {
|
||||||
super(props);
|
if (!inputValue) {
|
||||||
|
hasError = true;
|
||||||
this.state = {
|
errorMessage = "Field can't be empty.";
|
||||||
hotKeys: this.props.hotkeyDefinitions,
|
return {
|
||||||
errorMessages: {},
|
hasError,
|
||||||
};
|
errorMessage,
|
||||||
|
};
|
||||||
this.onInputKeyDown = this.onInputKeyDown.bind(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Normalizes the keys used in a KeyPress event and returns an array of the
|
|
||||||
* keys pressed
|
|
||||||
*
|
|
||||||
* @param {KeyDownEvent} keyDownEvent
|
|
||||||
* @returns {string[]}
|
|
||||||
*/
|
|
||||||
getKeysPressedArray(keyDownEvent) {
|
|
||||||
const keysPressedArray = [];
|
|
||||||
const { ctrlKey, altKey, shiftKey } = keyDownEvent;
|
|
||||||
|
|
||||||
if (ctrlKey && !altKey) {
|
|
||||||
keysPressedArray.push('ctrl');
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
if (shiftKey && !altKey) {
|
const conflictingValidator = ({
|
||||||
keysPressedArray.push('shift');
|
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,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (altKey && !ctrlKey) {
|
const disallowedValidator = ({ inputValue, pressedKeys, lastPressedKey }) => {
|
||||||
keysPressedArray.push('alt');
|
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,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return keysPressedArray;
|
const validators = [
|
||||||
}
|
emptyValidator,
|
||||||
|
modifierValidator,
|
||||||
|
conflictingValidator,
|
||||||
|
disallowedValidator,
|
||||||
|
];
|
||||||
|
|
||||||
getConflictingCommand(currentCommandName, currentHotKeys) {
|
for (const validator of validators) {
|
||||||
return this.state.hotKeys.find((tool, index) => {
|
const validation = validator({
|
||||||
const toolHotKeys = tool.keys[0];
|
commandName,
|
||||||
return (
|
inputValue,
|
||||||
toolHotKeys &&
|
pressedKeys,
|
||||||
toolHotKeys === currentHotKeys &&
|
lastPressedKey,
|
||||||
tool.commandName !== currentCommandName
|
originalHotKeys,
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
if (validation && validation.hasError) {
|
||||||
|
return validation;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// validation has passed successfully
|
||||||
*
|
return {
|
||||||
* @param {String} commandName
|
hasError,
|
||||||
* @param {KeyDownEvent} keyDownEvent
|
errorMessage,
|
||||||
* @param {Boolean} [displayPressedKey=false]
|
};
|
||||||
*/
|
};
|
||||||
updateInputText(commandName, keyDownEvent, displayPressedKey = false) {
|
|
||||||
const pressedKeys = this.getKeysPressedArray(keyDownEvent);
|
/**
|
||||||
|
* 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) {
|
if (displayPressedKey) {
|
||||||
const specialKeyName = specialKeys[keyDownEvent.which];
|
const specialKeyName = specialKeys[keyDownEvent.which];
|
||||||
@ -82,183 +240,213 @@ export class HotKeysPreferences extends Component {
|
|||||||
specialKeyName ||
|
specialKeyName ||
|
||||||
keyDownEvent.key ||
|
keyDownEvent.key ||
|
||||||
String.fromCharCode(keyDownEvent.keyCode);
|
String.fromCharCode(keyDownEvent.keyCode);
|
||||||
|
|
||||||
|
// ensure lowerCase
|
||||||
pressedKeys.push(keyName.toLowerCase());
|
pressedKeys.push(keyName.toLowerCase());
|
||||||
}
|
}
|
||||||
|
|
||||||
this.updateHotKeysState(commandName, pressedKeys.join('+'));
|
setInputValue(formatPressedKeys(pressedKeys));
|
||||||
}
|
};
|
||||||
|
|
||||||
updateHotKeysState(commandName, keys) {
|
// validate input value
|
||||||
const hotKeys = this.state.hotKeys;
|
const validateInput = () => {
|
||||||
const hotKeyIndex = this.state.hotKeys.findIndex(
|
const pressedKeys = unFormatPressedKeys(inputValue);
|
||||||
x => x.commandName === commandName
|
const lastPressedKey = pressedKeys[pressedKeys.length - 1];
|
||||||
|
|
||||||
|
const {
|
||||||
|
hasError = false,
|
||||||
|
errorMessage = NO_FIELD_ERROR_MESSAGE,
|
||||||
|
} = inputValidators(
|
||||||
|
commandName,
|
||||||
|
inputValue,
|
||||||
|
pressedKeys,
|
||||||
|
lastPressedKey,
|
||||||
|
originalHotKeys
|
||||||
);
|
);
|
||||||
hotKeys[hotKeyIndex].keys[0] = keys;
|
|
||||||
this.setState({ hotKeys });
|
|
||||||
}
|
|
||||||
|
|
||||||
updateErrorsState(toolKey, errorMessage) {
|
if (hasError) {
|
||||||
const errorMessages = this.state.errorMessages;
|
setInputValue('');
|
||||||
errorMessages[toolKey] = errorMessage;
|
} else {
|
||||||
this.setState({ errorMessages });
|
onSuccessChanged([inputValue]);
|
||||||
}
|
}
|
||||||
|
|
||||||
onInputKeyDown(event, commandName) {
|
if (hasError !== error) {
|
||||||
|
setError(hasError);
|
||||||
|
}
|
||||||
|
|
||||||
|
setFieldErrorMessage(errorMessage);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
onFailureChanged(error);
|
||||||
|
}, [error]);
|
||||||
|
|
||||||
|
const onInputKeyDown = event => {
|
||||||
// Prevent ESC key from propagating and closing the modal
|
// Prevent ESC key from propagating and closing the modal
|
||||||
if (event.key === 'Escape') {
|
if (event.key === 'Escape') {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (allowedKeys.includes(event.keyCode)) {
|
updateInputText(event, allowedKeys.includes(event.keyCode));
|
||||||
this.updateInputText(commandName, event, true);
|
|
||||||
} else {
|
|
||||||
this.updateInputText(commandName, event, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}
|
};
|
||||||
|
|
||||||
onChange(event, commandName) {
|
return (
|
||||||
if (event.ctrlKey || event.altKey || event.shiftKey) {
|
<tr key={commandName}>
|
||||||
return;
|
<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);
|
||||||
}
|
}
|
||||||
|
|
||||||
const hotKeyIndex = this.state.hotKeys.findIndex(
|
if (tabErrorCounter === 1) {
|
||||||
x => x.commandName === commandName
|
onTabErrorChanged(name, true);
|
||||||
);
|
}
|
||||||
const hotKey = this.state.hotKeys[hotKeyIndex];
|
}, [tabErrorCounter]);
|
||||||
const keys = hotKey.keys[0];
|
|
||||||
const pressedKeys = keys.split('+');
|
|
||||||
const lastPressedKey = pressedKeys[pressedKeys.length - 1].toLowerCase();
|
|
||||||
|
|
||||||
// clear the prior errors
|
// update local state if parent updates
|
||||||
this.setState({ errorMessages: {} }, () => {
|
useEffect(() => {
|
||||||
// Check if it has a valid modifier
|
setTabState({ ...hotkeyDefinitions });
|
||||||
const isModifier = ['ctrl', 'alt', 'shift'].includes(lastPressedKey);
|
}, [hotkeyDefinitions]);
|
||||||
if (isModifier) {
|
|
||||||
this.updateHotKeysState(commandName, '');
|
|
||||||
this.updateErrorsState(
|
|
||||||
commandName,
|
|
||||||
"It's not possible to define only modifier keys (ctrl, alt and shift) as a shortcut"
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
return (
|
||||||
* Check if it has some conflict
|
<div className="HotKeysPreferences">
|
||||||
*/
|
{splittedHotKeys.length > 0
|
||||||
const conflictedCommand = this.getConflictingCommand(commandName, keys);
|
? splittedHotKeys.map((columnHotKeys, index) => {
|
||||||
if (conflictedCommand) {
|
return (
|
||||||
this.updateHotKeysState(commandName, '');
|
<div className="column" key={index}>
|
||||||
this.updateErrorsState(
|
<table className="full-width">
|
||||||
commandName,
|
<thead>
|
||||||
`"${conflictedCommand.label}" is already using the "${keys}" shortcut.`
|
<tr>
|
||||||
);
|
<th className="text-right p-r-1">Function</th>
|
||||||
return;
|
<th className="text-center">Shortcut</th>
|
||||||
}
|
</tr>
|
||||||
|
</thead>
|
||||||
/*
|
<tbody>
|
||||||
* Check if is a valid combination
|
{Object.entries(columnHotKeys).map(
|
||||||
*/
|
hotkeyDefinitionTuple => (
|
||||||
const modifierCommand = pressedKeys
|
<HotKeyPreferencesRow
|
||||||
.slice(0, pressedKeys.length - 1)
|
key={hotkeyDefinitionTuple[0]}
|
||||||
.join('+')
|
commandName={hotkeyDefinitionTuple[0]}
|
||||||
.toLowerCase();
|
hotkeys={hotkeyDefinitionTuple[1].keys}
|
||||||
|
label={hotkeyDefinitionTuple[1].label}
|
||||||
const disallowedCombination = disallowedCombinations[modifierCommand];
|
originalHotKeys={tabState}
|
||||||
const hasDisallowedCombinations = disallowedCombination
|
tabError={tabError}
|
||||||
? disallowedCombination.includes(lastPressedKey)
|
onSuccessChanged={keys =>
|
||||||
: false;
|
onHotKeyChanged(
|
||||||
|
hotkeyDefinitionTuple[0],
|
||||||
if (hasDisallowedCombinations) {
|
hotkeyDefinitionTuple[1],
|
||||||
this.updateHotKeysState(commandName, '');
|
keys
|
||||||
this.updateErrorsState(
|
)
|
||||||
commandName,
|
}
|
||||||
`"${pressedKeys.join('+')}" shortcut combination is not allowed`
|
onFailureChanged={onErrorChanged}
|
||||||
);
|
></HotKeyPreferencesRow>
|
||||||
return;
|
)
|
||||||
}
|
)}
|
||||||
});
|
</tbody>
|
||||||
}
|
</table>
|
||||||
|
</div>
|
||||||
renderRow({ commandName, label, keys }) {
|
);
|
||||||
return (
|
})
|
||||||
<tr key={commandName}>
|
: null}
|
||||||
<td className="text-right p-r-1">{label}</td>
|
</div>
|
||||||
<td width="200">
|
);
|
||||||
<label
|
|
||||||
className={`wrapperLabel ${
|
|
||||||
this.state.errorMessages[commandName] !== undefined
|
|
||||||
? 'state-error'
|
|
||||||
: ''
|
|
||||||
} `}
|
|
||||||
ref={input => (this[commandName] = input)}
|
|
||||||
data-key="defaultTool"
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
readOnly={true}
|
|
||||||
type="text"
|
|
||||||
value={keys[0]}
|
|
||||||
vali="true"
|
|
||||||
className="form-control hotkey text-center"
|
|
||||||
onKeyDown={event => this.onInputKeyDown(event, commandName)}
|
|
||||||
onKeyUp={event => this.onChange(event, commandName)}
|
|
||||||
/>
|
|
||||||
<span className="wrapperText" />
|
|
||||||
<span className="errorMessage">
|
|
||||||
{this.state.errorMessages[commandName]}
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const halfWayThough = Math.floor(this.state.hotKeys.length / 2);
|
|
||||||
const firstHalfHotkeys = this.state.hotKeys.slice(0, halfWayThough);
|
|
||||||
const secondHalfHotkeys = this.state.hotKeys.slice(
|
|
||||||
halfWayThough,
|
|
||||||
this.state.hotKeys.length
|
|
||||||
);
|
|
||||||
|
|
||||||
return this.state.hotKeys.length > 0 ? (
|
|
||||||
<div className="HotKeysPreferences">
|
|
||||||
{/* <!-- Column 1 --> */}
|
|
||||||
<div className="column">
|
|
||||||
<table className="full-width">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th className="text-right p-r-1">Function</th>
|
|
||||||
<th className="text-center">Shortcut</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{firstHalfHotkeys.map(hotkeyDefinition =>
|
|
||||||
this.renderRow(hotkeyDefinition)
|
|
||||||
)}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
{/* <!-- Column 2 --> */}
|
|
||||||
<div className="column">
|
|
||||||
<table className="full-width">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th className="text-right p-r-1">Function</th>
|
|
||||||
<th className="text-center">Shortcut</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{secondHalfHotkeys.map(hotkeyDefinition =>
|
|
||||||
this.renderRow(hotkeyDefinition)
|
|
||||||
)}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<p>{`No hotkeys are configured for this application. Hotkeys can be configured in the application's app-config.js file.`}</p>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
HotKeysPreferences.propTypes = {
|
||||||
|
hotkeyDefinitions: PropTypes.any,
|
||||||
|
name: PropTypes.string,
|
||||||
|
tabError: PropTypes.bool,
|
||||||
|
onTabStateChanged: PropTypes.func,
|
||||||
|
onTabErrorChanged: PropTypes.func,
|
||||||
|
};
|
||||||
|
|
||||||
|
export { HotKeysPreferences };
|
||||||
|
|||||||
@ -10,7 +10,7 @@ export class UserPreferences extends Component {
|
|||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
hotkeyDefinitions: [],
|
hotkeyDefinitions: [],
|
||||||
windowLevelData: {},
|
windowLevelData: {},
|
||||||
generalData: {},
|
generalPreferences: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO: Make this more generic. Tabs should not be restricted to these entries
|
// TODO: Make this more generic. Tabs should not be restricted to these entries
|
||||||
@ -23,7 +23,8 @@ export class UserPreferences extends Component {
|
|||||||
})
|
})
|
||||||
).isRequired,
|
).isRequired,
|
||||||
windowLevelData: PropTypes.object.isRequired,
|
windowLevelData: PropTypes.object.isRequired,
|
||||||
generalData: PropTypes.object.isRequired,
|
generalPreferences: PropTypes.object.isRequired,
|
||||||
|
updatePropValue: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
@ -64,7 +65,10 @@ export class UserPreferences extends Component {
|
|||||||
return (
|
return (
|
||||||
<form className="form-themed themed">
|
<form className="form-themed themed">
|
||||||
<div className="form-content">
|
<div className="form-content">
|
||||||
<GeneralPreferences generalData={this.props.generalData} />
|
<GeneralPreferences
|
||||||
|
generalPreferences={this.props.generalPreferences}
|
||||||
|
updatePropValue={this.props.updatePropValue}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,101 +1,219 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { useSnackbarContext } from '@ohif/ui';
|
||||||
|
|
||||||
import './UserPreferencesForm.styl';
|
import './UserPreferencesForm.styl';
|
||||||
|
|
||||||
import React, { Component } from 'react';
|
import { useTranslation } from 'react-i18next';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { withTranslation } from '../../contextProviders';
|
|
||||||
|
|
||||||
import cloneDeep from 'lodash.clonedeep';
|
// Tabs Component wrapper
|
||||||
import isEqual from 'lodash.isequal';
|
import { UserPreferencesTabs } from './UserPreferencesTabs';
|
||||||
import { UserPreferences } from './UserPreferences';
|
|
||||||
|
|
||||||
class UserPreferencesForm extends Component {
|
// Tabs
|
||||||
// TODO: Make this component more generic to allow things other than W/L and hotkeys...
|
import { HotKeysPreferences } from './HotKeysPreferences';
|
||||||
static propTypes = {
|
import { WindowLevelPreferences } from './WindowLevelPreferences';
|
||||||
onClose: PropTypes.func,
|
import { GeneralPreferences } from './GeneralPreferences';
|
||||||
onSave: PropTypes.func,
|
|
||||||
onResetToDefaults: PropTypes.func,
|
/**
|
||||||
windowLevelData: PropTypes.object,
|
@typedef TabObject
|
||||||
hotkeyDefinitions: PropTypes.arrayOf(
|
@type {Object}
|
||||||
PropTypes.shape({
|
@property {string} name Name for given tab
|
||||||
commandName: PropTypes.string,
|
@property {ReactComponent} Component React component for given tab.
|
||||||
keys: PropTypes.arrayOf(PropTypes.string),
|
@property {object} props Props State for given tab component
|
||||||
label: PropTypes.string,
|
@property {boolean} [hidden] To hidden tab or not
|
||||||
})
|
*/
|
||||||
).isRequired,
|
|
||||||
t: PropTypes.func,
|
/**
|
||||||
|
* 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 },
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(props) {
|
const [tabsState, setTabsState] = useState(
|
||||||
super(props);
|
createTabsState(windowLevelData, hotkeyDefinitions, generalPreferences)
|
||||||
|
);
|
||||||
|
|
||||||
this.state = {
|
const [tabsError, setTabsError] = useState(
|
||||||
windowLevelData: cloneDeep(props.windowLevelData),
|
tabs.reduce((acc, tab) => {
|
||||||
hotkeyDefinitions: cloneDeep(props.hotkeyDefinitions),
|
acc[tab.name] = false;
|
||||||
};
|
return acc;
|
||||||
}
|
}, {})
|
||||||
|
);
|
||||||
|
|
||||||
save = () => {
|
const snackbar = useSnackbarContext();
|
||||||
this.props.onSave({
|
|
||||||
windowLevelData: this.state.windowLevelData,
|
const { t, ready: translationsAreReady } = useTranslation(
|
||||||
hotkeyDefinitions: this.state.hotkeyDefinitions,
|
'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',
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
componentDidUpdate(prev, next) {
|
const onSavePreferences = event => {
|
||||||
const newStateData = {};
|
const toSave = Object.values(tabsState).reduce((acc, tabState) => {
|
||||||
|
return { ...acc, ...tabState };
|
||||||
|
}, {});
|
||||||
|
|
||||||
if (!isEqual(prev.windowLevelData, next.windowLevelData)) {
|
onSave(toSave);
|
||||||
newStateData.windowLevelData = prev.windowLevelData;
|
snackbar.show({
|
||||||
}
|
message: t('SaveMessage'),
|
||||||
|
type: 'success',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
if (!isEqual(prev.hotkeyDefinitions, next.hotkeyDefinitions)) {
|
// update local state if prop values changes
|
||||||
newStateData.hotkeyDefinitions = prev.hotkeyDefinitions;
|
useEffect(() => {
|
||||||
}
|
setTabsState(
|
||||||
|
createTabsState(windowLevelData, hotkeyDefinitions, generalPreferences)
|
||||||
|
);
|
||||||
|
}, [windowLevelData, hotkeyDefinitions, generalPreferences]);
|
||||||
|
|
||||||
if (newStateData.hotkeyDefinitions || newStateData.windowLevelData) {
|
return translationsAreReady ? (
|
||||||
this.setState(newStateData);
|
<div className="UserPreferencesForm">
|
||||||
}
|
<UserPreferencesTabs
|
||||||
}
|
tabs={tabs}
|
||||||
|
tabsState={tabsState}
|
||||||
render() {
|
tabsError={tabsError}
|
||||||
return (
|
onTabStateChanged={onTabStateChanged}
|
||||||
<div className="UserPreferencesForm">
|
onTabErrorChanged={onTabErrorChanged}
|
||||||
<UserPreferences
|
/>
|
||||||
windowLevelData={this.state.windowLevelData}
|
<div className="footer">
|
||||||
hotkeyDefinitions={this.state.hotkeyDefinitions}
|
<button
|
||||||
/>
|
className="btn btn-danger pull-left"
|
||||||
<div className="footer">
|
data-cy="reset-default-btn"
|
||||||
<button
|
onClick={onResetPreferences}
|
||||||
className="btn btn-danger pull-left"
|
>
|
||||||
data-cy="reset-default-btn"
|
{t('Reset to Defaults')}
|
||||||
onClick={this.props.onResetToDefaults}
|
</button>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
onClick={onClose}
|
||||||
|
data-cy="cancel-btn"
|
||||||
|
className="btn btn-default"
|
||||||
>
|
>
|
||||||
{this.props.t('Reset to Defaults')}
|
{t('Cancel')}
|
||||||
</button>
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
onClick={this.props.onClose}
|
|
||||||
className="btn btn-default"
|
|
||||||
data-cy="cancel-btn"
|
|
||||||
>
|
|
||||||
{this.props.t('Cancel')}
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
className="btn btn-primary"
|
|
||||||
data-cy="save-btn"
|
|
||||||
onClick={this.save}
|
|
||||||
>
|
|
||||||
{this.props.t('Save')}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
<button
|
||||||
|
className="btn btn-primary"
|
||||||
|
data-cy="save-btn"
|
||||||
|
disabled={hasAnyError()}
|
||||||
|
onClick={onSavePreferences}
|
||||||
|
>
|
||||||
|
{t('Save')}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
</div>
|
||||||
}
|
) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const connectedComponent = withTranslation('UserPreferencesForm')(
|
UserPreferencesForm.propTypes = {
|
||||||
UserPreferencesForm
|
onClose: PropTypes.func,
|
||||||
);
|
onSave: PropTypes.func,
|
||||||
export { connectedComponent as UserPreferencesForm };
|
onResetToDefaults: PropTypes.func,
|
||||||
export default connectedComponent;
|
windowLevelData: PropTypes.object,
|
||||||
|
hotkeyDefinitions: PropTypes.object,
|
||||||
|
generalPreferences: PropTypes.object,
|
||||||
|
hotkeysManager: PropTypes.object,
|
||||||
|
defaultLanguage: PropTypes.string,
|
||||||
|
hotkeyDefaults: PropTypes.array,
|
||||||
|
};
|
||||||
|
|
||||||
|
export { UserPreferencesForm };
|
||||||
|
|||||||
@ -0,0 +1,105 @@
|
|||||||
|
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 tabClass =
|
||||||
|
index === activeTabIndex ? 'nav-link active' : 'nav-link';
|
||||||
|
return !hidden ? (
|
||||||
|
<li
|
||||||
|
key={name}
|
||||||
|
onClick={() => {
|
||||||
|
onHeaderChanged(index);
|
||||||
|
}}
|
||||||
|
className={tabClass}
|
||||||
|
>
|
||||||
|
<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 };
|
||||||
@ -3,7 +3,7 @@
|
|||||||
@import './../../design/styles/common/state.styl'
|
@import './../../design/styles/common/state.styl'
|
||||||
@import './../../design/styles/common/global.styl'
|
@import './../../design/styles/common/global.styl'
|
||||||
|
|
||||||
.UserPreferences
|
.UserPreferencesTabs
|
||||||
display: flex
|
display: flex
|
||||||
flex-direction: column
|
flex-direction: column
|
||||||
|
|
||||||
@ -1,94 +1,132 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import './WindowLevelPreferences.styl';
|
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 });
|
||||||
|
|
||||||
export class WindowLevelPreferences extends Component {
|
const onInputChanged = (event, name) => {
|
||||||
constructor(props) {
|
const newValue = event.target.value;
|
||||||
super(props);
|
setRowState({ ...rowState, [name]: newValue });
|
||||||
|
|
||||||
this.state = {
|
|
||||||
data: this.props.windowLevelData,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
windowLevelData: PropTypes.object.isRequired,
|
|
||||||
onChange: PropTypes.func,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
onChange(event, key, field) {
|
useEffect(() => {
|
||||||
const data = this.state.data;
|
onSuccessChanged(rowName, rowState);
|
||||||
const entry = data[key];
|
}, [rowState]);
|
||||||
entry[field] = event.target.value;
|
|
||||||
this.setState({ data });
|
|
||||||
|
|
||||||
if (this.props.onChange) {
|
const renderTd = (value, name, type) => {
|
||||||
this.props.onChange(data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getWLPreferencesRows(key) {
|
|
||||||
const entry = this.state.data[key];
|
|
||||||
return (
|
return (
|
||||||
<tr key={key}>
|
<td className="p-r-1">
|
||||||
<td className="p-r-1 text-center">{key}</td>
|
<label className="wrapperLabel">
|
||||||
<td className="p-r-1">
|
<input
|
||||||
<label className="wrapperLabel">
|
value={value}
|
||||||
<input
|
type={type}
|
||||||
value={entry.description}
|
className="form-control"
|
||||||
type="text"
|
onChange={event => {
|
||||||
className="form-control"
|
onInputChanged(event, name);
|
||||||
onChange={event => {
|
}}
|
||||||
this.onChange(event, key, 'description');
|
/>
|
||||||
}}
|
</label>
|
||||||
/>
|
</td>
|
||||||
</label>
|
|
||||||
</td>
|
|
||||||
<td className="p-r-1">
|
|
||||||
<label className="wrapperLabel">
|
|
||||||
<input
|
|
||||||
value={entry.window}
|
|
||||||
type="number"
|
|
||||||
className="form-control"
|
|
||||||
onChange={event => {
|
|
||||||
this.onChange(event, key, 'window');
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
</td>
|
|
||||||
<td className="p-r-1">
|
|
||||||
<label className="wrapperLabel">
|
|
||||||
<input
|
|
||||||
value={entry.level}
|
|
||||||
type="number"
|
|
||||||
className="form-control"
|
|
||||||
onChange={event => {
|
|
||||||
this.onChange(event, key, 'level');
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
render() {
|
return (
|
||||||
return (
|
<tr key={rowName}>
|
||||||
<table className="full-width">
|
<td className="p-r-1 text-center">{rowName}</td>
|
||||||
<thead>
|
{renderTd(rowState.description, 'description', 'text')}
|
||||||
<tr>
|
{renderTd(rowState.window, 'window', 'number')}
|
||||||
<th className="p-x-1 text-center presetIndex">Preset</th>
|
{renderTd(rowState.level, 'level', 'number')}
|
||||||
<th className="p-x-1">Description</th>
|
</tr>
|
||||||
<th className="p-x-1">Window</th>
|
);
|
||||||
<th className="p-x-1">Level</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{Object.keys(this.state.data).map(key => {
|
|
||||||
return this.getWLPreferencesRows(key);
|
|
||||||
})}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 +1,4 @@
|
|||||||
@import './UserPreferences.styl'
|
@import './UserPreferencesTabs.styl'
|
||||||
|
|
||||||
.presetIndex
|
.presetIndex
|
||||||
padding: 0px 10px 0px 10px
|
padding: 0px 10px 0px 10px
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
export { UserPreferences } from './UserPreferences.js';
|
export { UserPreferencesTabs } from './UserPreferencesTabs.js';
|
||||||
export { AboutContent } from '../content/aboutContent/AboutContent.js';
|
export { AboutContent } from '../content/aboutContent/AboutContent.js';
|
||||||
export { UserPreferencesForm } from './UserPreferencesForm.js';
|
export { UserPreferencesForm } from './UserPreferencesForm.js';
|
||||||
export { GeneralPreferences } from './GeneralPreferences.js';
|
export { GeneralPreferences } from './GeneralPreferences.js';
|
||||||
|
|||||||
@ -46,7 +46,7 @@ class DropdownMenu extends Component {
|
|||||||
<button
|
<button
|
||||||
key={key}
|
key={key}
|
||||||
className="dd-item"
|
className="dd-item"
|
||||||
data-cy="about-item-menu"
|
data-cy="dd-item-menu"
|
||||||
onClick={() => this.handleOnClick(onClick)}
|
onClick={() => this.handleOnClick(onClick)}
|
||||||
>
|
>
|
||||||
{icon && <Icon {...icon} className="dd-item-icon" />}
|
{icon && <Icon {...icon} className="dd-item-icon" />}
|
||||||
|
|||||||
@ -270,7 +270,7 @@ describe('OHIF Study Viewer Page', function() {
|
|||||||
cy.get('[data-cy="options-menu"]')
|
cy.get('[data-cy="options-menu"]')
|
||||||
.first()
|
.first()
|
||||||
.click();
|
.click();
|
||||||
cy.get('[data-cy="about-item-menu"]')
|
cy.get('[data-cy="dd-item-menu"]')
|
||||||
.first()
|
.first()
|
||||||
.click();
|
.click();
|
||||||
cy.get('[data-cy="about-modal"]')
|
cy.get('[data-cy="about-modal"]')
|
||||||
|
|||||||
@ -59,91 +59,89 @@ describe('OHIF User Preferences', () => {
|
|||||||
.should('have.text', 'SOLO USO PARA INVESTIGACIÓN');
|
.should('have.text', 'SOLO USO PARA INVESTIGACIÓN');
|
||||||
|
|
||||||
// Options menu should be translated
|
// Options menu should be translated
|
||||||
cy.get('[data-cy="options-menu"]').should('have.text', 'Opciones');
|
cy.get('[data-cy="options-menu"]')
|
||||||
|
.should('have.text', 'Opciones')
|
||||||
|
.click();
|
||||||
|
|
||||||
//TODO: the following code is blocked by issue 1193: https://github.com/OHIF/Viewers/issues/1193
|
cy.get('[data-cy="dd-item-menu"]')
|
||||||
//Once the issue is fixed, the following code should be uncommented
|
.first()
|
||||||
// cy.get('[data-cy="about-item-menu"]')
|
.should('contain.text', 'Acerca de');
|
||||||
// .first()
|
cy.get('[data-cy="dd-item-menu"]')
|
||||||
// .should('have.text', 'Acerca de');
|
.last()
|
||||||
// cy.get('[data-cy="about-item-menu"]')
|
.should('contain.text', 'Preferencias');
|
||||||
// .last()
|
|
||||||
// .should('have.text', 'Preferencias');
|
// Close Options menu
|
||||||
|
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', function() {
|
||||||
cy.get('@userPreferencesGeneralTab')
|
// Set language to English and save
|
||||||
.click()
|
cy.setLanguage('English');
|
||||||
.should('have.class', 'active');
|
|
||||||
|
|
||||||
// Language dropdown should be displayed
|
// Set language to Spanish and cancel
|
||||||
cy.get('#language-select').should('be.visible');
|
cy.setLanguage('Spanish', false);
|
||||||
|
|
||||||
// Select Spanish and Cancel
|
// Header should be kept in English
|
||||||
cy.get('#language-select').select('Spanish');
|
cy.get('.research-use')
|
||||||
cy.get('@cancelBtn')
|
.scrollIntoView()
|
||||||
|
.should('have.text', 'INVESTIGATIONAL USE ONLY');
|
||||||
|
|
||||||
|
// Options menu should be translated
|
||||||
|
cy.get('[data-cy="options-menu"]')
|
||||||
|
.should('have.text', 'Options')
|
||||||
|
.click();
|
||||||
|
|
||||||
|
cy.get('[data-cy="dd-item-menu"]')
|
||||||
|
.first()
|
||||||
|
.should('contain.text', 'About');
|
||||||
|
cy.get('[data-cy="dd-item-menu"]')
|
||||||
|
.last()
|
||||||
|
.should('contain.text', 'Preferences');
|
||||||
|
|
||||||
|
// Close Options menu
|
||||||
|
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() {
|
||||||
|
// Set language to Spanish
|
||||||
|
cy.setLanguage('Spanish');
|
||||||
|
|
||||||
|
//Open Preferences again
|
||||||
|
cy.openPreferences();
|
||||||
|
|
||||||
|
// Go to general tab
|
||||||
|
cy.get('@userPreferencesGeneralTab').click();
|
||||||
|
|
||||||
|
cy.get('@restoreBtn')
|
||||||
.scrollIntoView()
|
.scrollIntoView()
|
||||||
.click();
|
.click();
|
||||||
|
|
||||||
// TODO: the following code is blocked by issue 1193: https://github.com/OHIF/Viewers/issues/1193
|
// Save
|
||||||
// Once the issue is fixed, the following code should be uncommented
|
cy.get('@saveBtn')
|
||||||
// // Header should be kept in English
|
.scrollIntoView()
|
||||||
// cy.get('.research-use')
|
.click();
|
||||||
// .scrollIntoView()
|
|
||||||
// .should('have.text', 'INVESTIGATIONAL USE ONLY');
|
// Header should be in English
|
||||||
//
|
cy.get('.research-use')
|
||||||
// // Options menu should be translated
|
.scrollIntoView()
|
||||||
// cy.get('[data-cy="options-menu"]').should('have.text', 'Options');
|
.should('have.text', 'INVESTIGATIONAL USE ONLY');
|
||||||
//
|
|
||||||
// cy.get('[data-cy="about-item-menu"]')
|
// Options menu should be in English
|
||||||
// .first()
|
cy.get('[data-cy="options-menu"]')
|
||||||
// .should('have.text', 'About');
|
.should('have.text', 'Options')
|
||||||
// cy.get('[data-cy="about-item-menu"]')
|
.click();
|
||||||
// .last()
|
|
||||||
// .should('have.text', 'Preferences');
|
cy.get('[data-cy="dd-item-menu"]')
|
||||||
|
.first()
|
||||||
|
.should('contain.text', 'About');
|
||||||
|
cy.get('[data-cy="dd-item-menu"]')
|
||||||
|
.last()
|
||||||
|
.should('contain.text', 'Preferences');
|
||||||
|
|
||||||
|
// Close options Menu
|
||||||
|
cy.get('[data-cy="options-menu"]').click();
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO: the following code is blocked by issue 1193: https://github.com/OHIF/Viewers/issues/1193
|
|
||||||
// Once the issue is fixed, the following code should be uncommented
|
|
||||||
// it('checks if user can restore to default the language selection and application will be in English', function() {
|
|
||||||
// cy.get('@userPreferencesGeneralTab')
|
|
||||||
// .click()
|
|
||||||
// .should('have.class', 'active');
|
|
||||||
|
|
||||||
// // Language dropdown should be displayed
|
|
||||||
// cy.get('#language-select').should('be.visible');
|
|
||||||
|
|
||||||
// // Select Spanish and Save
|
|
||||||
// cy.get('#language-select').select('Spanish');
|
|
||||||
// cy.get('@saveBtn')
|
|
||||||
// .scrollIntoView()
|
|
||||||
// .click();
|
|
||||||
|
|
||||||
// //Open Preferences again
|
|
||||||
// cy.openPreferences();
|
|
||||||
|
|
||||||
// // Go to general tab
|
|
||||||
// cy.get('@userPreferencesGeneralTab').click();
|
|
||||||
|
|
||||||
// cy.get('@restoreBtn')
|
|
||||||
// .scrollIntoView()
|
|
||||||
// .click();
|
|
||||||
|
|
||||||
// Header should be in English
|
|
||||||
// cy.get('.research-use')
|
|
||||||
// .scrollIntoView()
|
|
||||||
// .should('have.text', 'INVESTIGATIONAL USE ONLY');
|
|
||||||
//
|
|
||||||
// Options menu should be in English
|
|
||||||
// cy.get('[data-cy="options-menu"]').should('have.text', 'Options');
|
|
||||||
// cy.get('[data-cy="about-item-menu"]')
|
|
||||||
// .first()
|
|
||||||
// .should('have.text', 'About');
|
|
||||||
// cy.get('[data-cy="about-item-menu"]')
|
|
||||||
// .last()
|
|
||||||
// .should('have.text', '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.get('@userPreferencesHotkeysTab')
|
||||||
@ -185,16 +183,12 @@ describe('OHIF User Preferences', () => {
|
|||||||
cy.get('[data-cy="options-menu"]')
|
cy.get('[data-cy="options-menu"]')
|
||||||
.should('have.text', 'Opciones')
|
.should('have.text', 'Opciones')
|
||||||
.click();
|
.click();
|
||||||
cy.get('[data-cy="about-item-menu"]')
|
cy.get('[data-cy="dd-item-menu"]')
|
||||||
.first()
|
.first()
|
||||||
.should('contain.text', 'Acerca de');
|
.should('contain.text', 'Acerca de');
|
||||||
cy.get('[data-cy="options-menu"]').click(); //Close Options overlay
|
cy.get('[data-cy="dd-item-menu"]')
|
||||||
|
.last()
|
||||||
//TODO: the following code is blocked by issue 1193: https://github.com/OHIF/Viewers/issues/1193
|
.should('contain.text', 'Preferencias');
|
||||||
//Once the issue is fixed, the following code should be uncommented
|
|
||||||
// cy.get('[data-cy="about-item-menu"]')
|
|
||||||
// .last()
|
|
||||||
// .should('have.text', 'Preferencias');
|
|
||||||
|
|
||||||
// Check if new hotkey is working on viewport
|
// Check if new hotkey is working on viewport
|
||||||
cy.get('body').type('{shift}Q', { release: false });
|
cy.get('body').type('{shift}Q', { release: false });
|
||||||
@ -266,19 +260,49 @@ describe('OHIF User Preferences', () => {
|
|||||||
.should('have.text', 'SOLO USO PARA INVESTIGACIÓN');
|
.should('have.text', 'SOLO USO PARA INVESTIGACIÓN');
|
||||||
|
|
||||||
// Options menu should be translated
|
// Options menu should be translated
|
||||||
cy.get('[data-cy="options-menu"]').should('have.text', 'Opciones');
|
cy.get('[data-cy="options-menu"]')
|
||||||
|
.should('have.text', 'Opciones')
|
||||||
|
.click();
|
||||||
|
|
||||||
//TODO: the following code is blocked by issue 1193: https://github.com/OHIF/Viewers/issues/1193
|
cy.get('[data-cy="dd-item-menu"]')
|
||||||
//Once the issue is fixed, the following code should be uncommented
|
.first()
|
||||||
// cy.get('[data-cy="about-item-menu"]')
|
.should('contain.text', 'Acerca de');
|
||||||
// .first()
|
cy.get('[data-cy="dd-item-menu"]')
|
||||||
// .should('have.text', 'Acerca de');
|
.last()
|
||||||
// cy.get('[data-cy="about-item-menu"]')
|
.should('contain.text', 'Preferencias');
|
||||||
// .last()
|
|
||||||
// .should('have.text', 'Preferencias');
|
// Close Options menu
|
||||||
|
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', function() {
|
||||||
|
// Set language to English and save
|
||||||
|
cy.setLanguage('English');
|
||||||
|
|
||||||
|
// Set language to Spanish and cancel
|
||||||
|
cy.setLanguage('Spanish', false);
|
||||||
|
|
||||||
|
// Header should be kept in English
|
||||||
|
cy.get('.research-use')
|
||||||
|
.scrollIntoView()
|
||||||
|
.should('have.text', 'INVESTIGATIONAL USE ONLY');
|
||||||
|
|
||||||
|
// Options menu should be translated
|
||||||
|
cy.get('[data-cy="options-menu"]')
|
||||||
|
.should('have.text', 'Options')
|
||||||
|
.click();
|
||||||
|
|
||||||
|
cy.get('[data-cy="dd-item-menu"]')
|
||||||
|
.first()
|
||||||
|
.should('contain.text', 'About');
|
||||||
|
cy.get('[data-cy="dd-item-menu"]')
|
||||||
|
.last()
|
||||||
|
.should('contain.text', 'Preferences');
|
||||||
|
// Close Options menu
|
||||||
|
cy.get('[data-cy="options-menu"]').click();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('checks if user can restore to default the language selection and application will be in English', function() {
|
||||||
cy.get('@userPreferencesGeneralTab')
|
cy.get('@userPreferencesGeneralTab')
|
||||||
.click()
|
.click()
|
||||||
.should('have.class', 'active');
|
.should('have.class', 'active');
|
||||||
@ -286,76 +310,51 @@ describe('OHIF User Preferences', () => {
|
|||||||
// 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 Spanish and Cancel
|
// Select Spanish and Save
|
||||||
cy.get('#language-select').select('Spanish');
|
cy.get('#language-select').select('Spanish');
|
||||||
cy.get('@cancelBtn')
|
cy.get('@saveBtn')
|
||||||
.scrollIntoView()
|
.scrollIntoView()
|
||||||
.click();
|
.click();
|
||||||
|
|
||||||
// TODO: the following code is blocked by issue 1193: https://github.com/OHIF/Viewers/issues/1193
|
//Open Preferences again
|
||||||
// Once the issue is fixed, the following code should be uncommented
|
cy.get('[data-cy="options-menu"]')
|
||||||
// // Header should be kept in English
|
.scrollIntoView()
|
||||||
// cy.get('.research-use')
|
.click();
|
||||||
// .scrollIntoView()
|
cy.get('[data-cy="dd-item-menu"]')
|
||||||
// .should('have.text', 'INVESTIGATIONAL USE ONLY');
|
.last()
|
||||||
//
|
.click();
|
||||||
// // Options menu should be translated
|
|
||||||
// cy.get('[data-cy="options-menu"]').should('have.text', 'Options');
|
// Go to general tab
|
||||||
//
|
cy.get('@userPreferencesGeneralTab').click();
|
||||||
// cy.get('[data-cy="about-item-menu"]')
|
|
||||||
// .first()
|
cy.get('@restoreBtn')
|
||||||
// .should('have.text', 'About');
|
.scrollIntoView()
|
||||||
// cy.get('[data-cy="about-item-menu"]')
|
.click();
|
||||||
// .last()
|
|
||||||
// .should('have.text', 'Preferences');
|
cy.get('@saveBtn')
|
||||||
|
.scrollIntoView()
|
||||||
|
.click();
|
||||||
|
|
||||||
|
// Header should be in English
|
||||||
|
cy.get('.research-use')
|
||||||
|
.scrollIntoView()
|
||||||
|
.should('have.text', 'INVESTIGATIONAL USE ONLY');
|
||||||
|
|
||||||
|
// Options menu should be in English
|
||||||
|
cy.get('[data-cy="options-menu"]')
|
||||||
|
.should('have.text', 'Options')
|
||||||
|
.click();
|
||||||
|
cy.get('[data-cy="dd-item-menu"]')
|
||||||
|
.first()
|
||||||
|
.should('contain.text', 'About');
|
||||||
|
cy.get('[data-cy="dd-item-menu"]')
|
||||||
|
.last()
|
||||||
|
.should('contain.text', 'Preferences');
|
||||||
|
|
||||||
|
// Close Options menu
|
||||||
|
cy.get('[data-cy="options-menu"]').click();
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO: the following code is blocked by issue 1193: https://github.com/OHIF/Viewers/issues/1193
|
|
||||||
// Once the issue is fixed, the following code should be uncommented
|
|
||||||
// it('checks if user can restore to default the language selection and application will be in English', function() {
|
|
||||||
// cy.get('@userPreferencesGeneralTab')
|
|
||||||
// .click()
|
|
||||||
// .should('have.class', 'active');
|
|
||||||
|
|
||||||
// // Language dropdown should be displayed
|
|
||||||
// cy.get('#language-select').should('be.visible');
|
|
||||||
|
|
||||||
// // Select Spanish and Save
|
|
||||||
// cy.get('#language-select').select('Spanish');
|
|
||||||
// cy.get('@saveBtn')
|
|
||||||
// .scrollIntoView()
|
|
||||||
// .click();
|
|
||||||
|
|
||||||
// //Open Preferences again
|
|
||||||
// cy.get('[data-cy="options-menu"]')
|
|
||||||
// .scrollIntoView()
|
|
||||||
// .click();
|
|
||||||
// cy.get('[data-cy="about-item-menu"]')
|
|
||||||
// .last()
|
|
||||||
// .click();
|
|
||||||
|
|
||||||
// // Go to general tab
|
|
||||||
// cy.get('@userPreferencesGeneralTab').click();
|
|
||||||
|
|
||||||
// cy.get('@restoreBtn')
|
|
||||||
// .scrollIntoView()
|
|
||||||
// .click();
|
|
||||||
|
|
||||||
// Header should be in English
|
|
||||||
// cy.get('.research-use')
|
|
||||||
// .scrollIntoView()
|
|
||||||
// .should('have.text', 'INVESTIGATIONAL USE ONLY');
|
|
||||||
//
|
|
||||||
// Options menu should be in English
|
|
||||||
// cy.get('[data-cy="options-menu"]').should('have.text', 'Options');
|
|
||||||
// cy.get('[data-cy="about-item-menu"]')
|
|
||||||
// .first()
|
|
||||||
// .should('have.text', 'About');
|
|
||||||
// cy.get('[data-cy="about-item-menu"]')
|
|
||||||
// .last()
|
|
||||||
// .should('have.text', '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.get('@userPreferencesHotkeysTab')
|
||||||
|
|||||||
@ -427,15 +427,19 @@ Cypress.Commands.add('initPreferencesModalAliases', () => {
|
|||||||
Cypress.Commands.add('openPreferences', () => {
|
Cypress.Commands.add('openPreferences', () => {
|
||||||
cy.log('Open User Preferences Modal');
|
cy.log('Open User Preferences Modal');
|
||||||
// Open User Preferences modal
|
// Open User Preferences modal
|
||||||
cy.get('[data-cy="options-menu"]')
|
cy.get('body').then(body => {
|
||||||
.scrollIntoView()
|
if (body.find('.OHIFModal').length === 0) {
|
||||||
.click()
|
cy.get('[data-cy="options-menu"]')
|
||||||
.then(() => {
|
.scrollIntoView()
|
||||||
cy.get('[data-cy="about-item-menu"]')
|
|
||||||
.last()
|
|
||||||
.click()
|
.click()
|
||||||
.wait(200);
|
.then(() => {
|
||||||
});
|
cy.get('[data-cy="dd-item-menu"]')
|
||||||
|
.last()
|
||||||
|
.click()
|
||||||
|
.wait(200);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
Cypress.Commands.add('resetUserHoktkeyPreferences', () => {
|
Cypress.Commands.add('resetUserHoktkeyPreferences', () => {
|
||||||
@ -446,10 +450,7 @@ Cypress.Commands.add('resetUserHoktkeyPreferences', () => {
|
|||||||
|
|
||||||
cy.log('Reset to Default Preferences');
|
cy.log('Reset to Default Preferences');
|
||||||
cy.get('@restoreBtn').click();
|
cy.get('@restoreBtn').click();
|
||||||
|
cy.get('@saveBtn').click();
|
||||||
//TODO: the following code is blocked by issue 1193: https://github.com/OHIF/Viewers/issues/1193
|
|
||||||
//Once the issue is fixed, the following code should be uncommented
|
|
||||||
//cy.get('@saveBtn').click();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
Cypress.Commands.add(
|
Cypress.Commands.add(
|
||||||
@ -457,11 +458,34 @@ 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').within(() => {
|
cy.get('.HotKeysPreferences')
|
||||||
cy.contains(function_label) // label we're looking for
|
.within(() => {
|
||||||
.parent()
|
cy.contains(function_label) // label we're looking for
|
||||||
.find('input') // closest input to that label
|
.parent()
|
||||||
.type(shortcut, { force: true }); // Set new shortcut for that function
|
.find('input') // closest input to that label
|
||||||
});
|
.type(shortcut, { force: true }); // Set new shortcut for that function
|
||||||
|
})
|
||||||
|
.blur();
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
Cypress.Commands.add('setLanguage', (language, save = true) => {
|
||||||
|
cy.openPreferences();
|
||||||
|
|
||||||
|
cy.get('@userPreferencesGeneralTab')
|
||||||
|
.click()
|
||||||
|
.should('have.class', 'active');
|
||||||
|
|
||||||
|
// Language dropdown should be displayed
|
||||||
|
cy.get('#language-select').should('be.visible');
|
||||||
|
|
||||||
|
// Select Language and Save/Cancel
|
||||||
|
cy.get('#language-select')
|
||||||
|
.select(language)
|
||||||
|
.then(() => {
|
||||||
|
const toClick = save ? '@saveBtn' : '@cancelBtn';
|
||||||
|
cy.get(toClick)
|
||||||
|
.scrollIntoView()
|
||||||
|
.click();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@ -23,6 +23,7 @@ import {
|
|||||||
createUIModalService,
|
createUIModalService,
|
||||||
createUIDialogService,
|
createUIDialogService,
|
||||||
utils,
|
utils,
|
||||||
|
redux as reduxOHIF
|
||||||
} from '@ohif/core';
|
} from '@ohif/core';
|
||||||
|
|
||||||
import i18n from '@ohif/i18n';
|
import i18n from '@ohif/i18n';
|
||||||
@ -45,6 +46,7 @@ import OHIFStandaloneViewer from './OHIFStandaloneViewer';
|
|||||||
/** Store */
|
/** Store */
|
||||||
import { getActiveContexts } from './store/layout/selectors.js';
|
import { getActiveContexts } from './store/layout/selectors.js';
|
||||||
import store from './store';
|
import store from './store';
|
||||||
|
const { setUserPreferences } = reduxOHIF.actions;
|
||||||
|
|
||||||
/** Contexts */
|
/** Contexts */
|
||||||
import WhiteLabellingContext from './context/WhiteLabellingContext';
|
import WhiteLabellingContext from './context/WhiteLabellingContext';
|
||||||
@ -221,9 +223,29 @@ function _initExtensions(extensions, hotkeys) {
|
|||||||
const mergedExtensions = defaultExtensions.concat(extensions);
|
const mergedExtensions = defaultExtensions.concat(extensions);
|
||||||
extensionManager.registerExtensions(mergedExtensions);
|
extensionManager.registerExtensions(mergedExtensions);
|
||||||
|
|
||||||
|
const { hotkeyDefinitions = {} } = store.getState().preferences || {};
|
||||||
|
let updateStore = false;
|
||||||
|
let hotkeysToUse = hotkeyDefinitions;
|
||||||
|
|
||||||
// Must run after extension commands are registered
|
// Must run after extension commands are registered
|
||||||
if (hotkeys) {
|
// if there is no hotkeys from localStorate set up from config
|
||||||
hotkeysManager.setHotkeys(hotkeys, true);
|
if (!Object.keys(hotkeyDefinitions).length) {
|
||||||
|
hotkeysToUse = hotkeys;
|
||||||
|
updateStore = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hotkeysToUse) {
|
||||||
|
hotkeysManager.setHotkeys(hotkeysToUse);
|
||||||
|
// set default based on app config
|
||||||
|
hotkeysManager.setDefaultHotKeys(hotkeys);
|
||||||
|
|
||||||
|
if (updateStore) {
|
||||||
|
const { hotkeyDefinitions } = hotkeysManager;
|
||||||
|
const windowLevelData = {};
|
||||||
|
store.dispatch(
|
||||||
|
setUserPreferences({ windowLevelData, hotkeyDefinitions })
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import React, { Component } 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';
|
||||||
@ -11,49 +11,37 @@ import './Header.css';
|
|||||||
// Context
|
// Context
|
||||||
import AppContext from './../../context/AppContext';
|
import AppContext from './../../context/AppContext';
|
||||||
|
|
||||||
class Header extends Component {
|
function Header(props) {
|
||||||
static contextType = AppContext;
|
const {
|
||||||
static propTypes = {
|
t,
|
||||||
home: PropTypes.bool.isRequired,
|
user,
|
||||||
location: PropTypes.object.isRequired,
|
userManager,
|
||||||
children: PropTypes.node,
|
modal: { show },
|
||||||
t: PropTypes.func.isRequired,
|
home,
|
||||||
userManager: PropTypes.object,
|
location,
|
||||||
user: PropTypes.object,
|
children,
|
||||||
modal: PropTypes.object,
|
} = props;
|
||||||
};
|
|
||||||
|
|
||||||
static defaultProps = {
|
const [options, setOptions] = useState([]);
|
||||||
home: true,
|
|
||||||
children: OHIFLogo(),
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor(props) {
|
useEffect(() => {
|
||||||
super(props);
|
const optionsValue = [
|
||||||
this.state = { isOpen: false };
|
|
||||||
|
|
||||||
this.loadOptions();
|
|
||||||
}
|
|
||||||
|
|
||||||
loadOptions() {
|
|
||||||
const { t, user, userManager, modal } = this.props;
|
|
||||||
this.options = [
|
|
||||||
{
|
{
|
||||||
title: t('About'),
|
title: t('About'),
|
||||||
icon: { name: 'info' },
|
icon: { name: 'info' },
|
||||||
onClick: () =>
|
onClick: () =>
|
||||||
modal.show({
|
show({
|
||||||
content: AboutContent,
|
content: AboutContent,
|
||||||
title: t('OHIF Viewer - About'),
|
title: t('OHIF Viewer - About'),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Preferences ',
|
title: t('Preferences'),
|
||||||
icon: {
|
icon: {
|
||||||
name: 'user',
|
name: 'user',
|
||||||
},
|
},
|
||||||
onClick: () =>
|
onClick: () =>
|
||||||
modal.show({
|
show({
|
||||||
content: ConnectedUserPreferencesForm,
|
content: ConnectedUserPreferencesForm,
|
||||||
title: t('User Preferences'),
|
title: t('User Preferences'),
|
||||||
}),
|
}),
|
||||||
@ -61,61 +49,74 @@ class Header extends Component {
|
|||||||
];
|
];
|
||||||
|
|
||||||
if (user && userManager) {
|
if (user && userManager) {
|
||||||
this.options.push({
|
optionsValue.push({
|
||||||
title: t('Logout'),
|
title: t('Logout'),
|
||||||
icon: { name: 'power-off' },
|
icon: { name: 'power-off' },
|
||||||
onClick: () => userManager.signoutRedirect(),
|
onClick: () => userManager.signoutRedirect(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
setOptions(optionsValue);
|
||||||
|
}, [setOptions, show, t, user, userManager]);
|
||||||
|
|
||||||
|
const { appConfig = {} } = AppContext;
|
||||||
|
const showStudyList =
|
||||||
|
appConfig.showStudyList !== undefined ? appConfig.showStudyList : true;
|
||||||
|
|
||||||
// ANTD -- Hamburger, Drawer, Menu
|
// ANTD -- Hamburger, Drawer, Menu
|
||||||
render() {
|
return (
|
||||||
const { t, home, location, children } = this.props;
|
<>
|
||||||
const { appConfig = {} } = this.context;
|
<div className="notification-bar">{t('INVESTIGATIONAL USE ONLY')}</div>
|
||||||
const showStudyList =
|
<div className={`entry-header ${home ? 'header-big' : ''}`}>
|
||||||
appConfig.showStudyList !== undefined ? appConfig.showStudyList : true;
|
<div className="header-left-box">
|
||||||
return (
|
{location && location.studyLink && (
|
||||||
<>
|
<Link
|
||||||
<div className="notification-bar">{t('INVESTIGATIONAL USE ONLY')}</div>
|
to={location.studyLink}
|
||||||
<div className={`entry-header ${home ? 'header-big' : ''}`}>
|
className="header-btn header-viewerLink"
|
||||||
<div className="header-left-box">
|
>
|
||||||
{location && location.studyLink && (
|
{t('Back to Viewer')}
|
||||||
<Link
|
</Link>
|
||||||
to={location.studyLink}
|
)}
|
||||||
className="header-btn header-viewerLink"
|
|
||||||
>
|
|
||||||
{t('Back to Viewer')}
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{children}
|
{children}
|
||||||
|
|
||||||
{showStudyList && !home && (
|
{showStudyList && !home && (
|
||||||
<Link
|
<Link
|
||||||
className="header-btn header-studyListLinkSection"
|
className="header-btn header-studyListLinkSection"
|
||||||
to={{
|
to={{
|
||||||
pathname: '/',
|
pathname: '/',
|
||||||
state: { studyLink: location.pathname },
|
state: { studyLink: location.pathname },
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t('Study list')}
|
{t('Study list')}
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="header-menu">
|
|
||||||
<span className="research-use">
|
|
||||||
{t('INVESTIGATIONAL USE ONLY')}
|
|
||||||
</span>
|
|
||||||
<Dropdown title={t('Options')} list={this.options} align="right" />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
|
||||||
);
|
<div className="header-menu">
|
||||||
}
|
<span className="research-use">{t('INVESTIGATIONAL USE ONLY')}</span>
|
||||||
|
<Dropdown title={t('Options')} list={options} align="right" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Header.propTypes = {
|
||||||
|
home: PropTypes.bool.isRequired,
|
||||||
|
location: PropTypes.object.isRequired,
|
||||||
|
children: PropTypes.node,
|
||||||
|
t: PropTypes.func.isRequired,
|
||||||
|
userManager: PropTypes.object,
|
||||||
|
user: PropTypes.object,
|
||||||
|
modal: PropTypes.object,
|
||||||
|
};
|
||||||
|
|
||||||
|
Header.defaultProps = {
|
||||||
|
home: true,
|
||||||
|
children: OHIFLogo(),
|
||||||
|
};
|
||||||
|
|
||||||
export default withTranslation(['Header', 'AboutModal'])(
|
export default withTranslation(['Header', 'AboutModal'])(
|
||||||
withRouter(withModal(Header))
|
withRouter(withModal(Header))
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,17 +1,7 @@
|
|||||||
import Header from '../components/Header/Header.js';
|
import Header from '../components/Header/Header.js';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { hotkeysManager } from '../App.js';
|
|
||||||
|
|
||||||
const mapStateToProps = state => {
|
const mapStateToProps = state => {
|
||||||
const { hotkeyDefinitions = [] } = state.preferences || {};
|
|
||||||
|
|
||||||
const _hotkeyDefinitions =
|
|
||||||
hotkeyDefinitions.length > 0
|
|
||||||
? hotkeyDefinitions
|
|
||||||
: hotkeysManager.hotkeyDefaults;
|
|
||||||
|
|
||||||
hotkeysManager.setHotkeys(_hotkeyDefinitions);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
user: state.oidc && state.oidc.user,
|
user: state.oidc && state.oidc.user,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,34 +1,48 @@
|
|||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { UserPreferencesForm } from '@ohif/ui';
|
import { UserPreferencesForm } from '@ohif/ui';
|
||||||
import OHIF from '@ohif/core';
|
import OHIF from '@ohif/core';
|
||||||
|
import i18n from '@ohif/i18n';
|
||||||
|
|
||||||
import { hotkeysManager } from '../App.js';
|
import { hotkeysManager } from '../App.js';
|
||||||
|
|
||||||
const { setUserPreferences } = OHIF.redux.actions;
|
const { setUserPreferences } = OHIF.redux.actions;
|
||||||
|
|
||||||
const mapStateToProps = (state, ownProps) => {
|
const mapStateToProps = (state, ownProps) => {
|
||||||
const hotkeyDefinitions =
|
const { defaultLanguage } = i18n;
|
||||||
state.preferences.hotkeyDefinitions.length > 0
|
const { hotkeyDefinitions, windowLevelData = {}, generalPreferences } =
|
||||||
? state.preferences.hotkeyDefinitions
|
state.preferences || {};
|
||||||
: hotkeysManager.hotkeyDefaults;
|
const { hotkeyDefaults } = hotkeysManager;
|
||||||
hotkeysManager.setHotkeys(hotkeyDefinitions);
|
|
||||||
return {
|
return {
|
||||||
onClose: ownProps.hide,
|
onClose: ownProps.hide,
|
||||||
windowLevelData: state.preferences ? state.preferences.windowLevelData : {},
|
windowLevelData,
|
||||||
hotkeyDefinitions,
|
hotkeyDefinitions,
|
||||||
|
generalPreferences,
|
||||||
|
hotkeysManager,
|
||||||
|
hotkeyDefaults,
|
||||||
|
defaultLanguage,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch, ownProps) => {
|
const mapDispatchToProps = (dispatch, ownProps) => {
|
||||||
return {
|
return {
|
||||||
onSave: ({ windowLevelData, hotkeyDefinitions }) => {
|
onSave: ({ windowLevelData, hotkeyDefinitions, generalPreferences }) => {
|
||||||
|
// TODO improve this strategy on windowLevel implementation
|
||||||
hotkeysManager.setHotkeys(hotkeyDefinitions);
|
hotkeysManager.setHotkeys(hotkeyDefinitions);
|
||||||
|
|
||||||
|
const { language } = generalPreferences;
|
||||||
|
|
||||||
|
// set new language
|
||||||
|
i18n.changeLanguage(language);
|
||||||
|
|
||||||
ownProps.hide();
|
ownProps.hide();
|
||||||
dispatch(setUserPreferences({ windowLevelData, hotkeyDefinitions }));
|
dispatch(
|
||||||
},
|
setUserPreferences({
|
||||||
onResetToDefaults: () => {
|
windowLevelData,
|
||||||
hotkeysManager.restoreDefaultBindings();
|
hotkeyDefinitions,
|
||||||
ownProps.hide();
|
generalPreferences,
|
||||||
dispatch(setUserPreferences());
|
})
|
||||||
|
);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@ -30,7 +30,7 @@
|
|||||||
--snackbar-info: rgba(54, 156, 199, 0.9);
|
--snackbar-info: rgba(54, 156, 199, 0.9);
|
||||||
|
|
||||||
/** z-Index **/
|
/** z-Index **/
|
||||||
--snackbar-zIndex: 10;
|
--snackbar-zIndex: 1075;
|
||||||
|
|
||||||
/** DatePicker Colors **/
|
/** DatePicker Colors **/
|
||||||
--datePicker-input-background: #2c363f;
|
--datePicker-input-background: #2c363f;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user