OHIF-332: Users should be able to see all available hotkeys and language settings in one place (#1895)
* OHIF-330: Update Modal Styles * feat/ohif-332: finish raw ui * feat/ohif-332: update mode configuration strategy * feat/ohif-332: fix hotkey errors * feat/ohif-332: update hotkey logic with recent merged changes * feat/ohif-322: wrap * feat/ohif-322: add disable state * ohif-332: cr updates * ohif-332: disable * ohif-332: cr updates * ohif-332: extract header component * ohif-332: cr update to fix merge conflicts and design issue Co-authored-by: Rodrigo Antinarelli <rodrigoantinarelli@gmail.com>
This commit is contained in:
parent
ae3d05ebf7
commit
3e944780dc
@ -172,6 +172,7 @@ project.
|
||||
const extensionManager = new ExtensionManager({
|
||||
commandsManager,
|
||||
servicesManager,
|
||||
hotkeysManager
|
||||
});
|
||||
|
||||
// prettier-ignore
|
||||
|
||||
@ -1,102 +0,0 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
// TODO: This may fail if package is split from PWA build
|
||||
import { useHistory } from 'react-router-dom';
|
||||
//
|
||||
import { NavBar, Svg, Icon, IconButton, Dropdown, useModal } from '@ohif/ui';
|
||||
|
||||
function Header({ children }) {
|
||||
const { t } = useTranslation();
|
||||
const history = useHistory();
|
||||
const { show } = useModal();
|
||||
|
||||
// TODO: IT SHOULD BE REFACTORED WHEN THE MODAL CONTENT IS DEFINED
|
||||
const showAboutModal = useCallback(() => {
|
||||
const modalComponent = () => (
|
||||
<div>{t('AboutModal:OHIF Viewer - About')}</div>
|
||||
);
|
||||
show({
|
||||
title: t('AboutModal:OHIF Viewer - About'),
|
||||
content: modalComponent,
|
||||
});
|
||||
}, [show, t]);
|
||||
|
||||
// TODO: IT SHOULD BE REFACTORED WHEN THE MODAL CONTENT IS DEFINED
|
||||
const showPreferencesModal = useCallback(() => {
|
||||
const modalComponent = () => (
|
||||
<div>{t('UserPreferencesModal:User Preferences')}</div>
|
||||
);
|
||||
show({
|
||||
title: t('UserPreferencesModal:User Preferences'),
|
||||
content: modalComponent,
|
||||
});
|
||||
}, [show, t]);
|
||||
|
||||
return (
|
||||
<NavBar className="justify-between border-b-4 border-black">
|
||||
<div className="flex justify-between flex-1">
|
||||
<div className="flex items-center">
|
||||
{/* // TODO: Should preserve filter/sort
|
||||
// Either injected service? Or context (like react router's `useLocation`?) */}
|
||||
<div
|
||||
className="inline-flex items-center mr-3"
|
||||
onClick={() => history.push('/')}
|
||||
>
|
||||
<Icon
|
||||
name="chevron-left"
|
||||
className="w-8 cursor-pointer text-primary-active"
|
||||
/>
|
||||
<div className="ml-4 cursor-pointer">
|
||||
<Svg name="logo-ohif" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center">{children}</div>
|
||||
<div className="flex items-center">
|
||||
<span className="mr-3 text-lg text-common-light">
|
||||
{t('Header:INVESTIGATIONAL USE ONLY')}
|
||||
</span>
|
||||
<Dropdown
|
||||
showDropdownIcon={false}
|
||||
list={[
|
||||
{
|
||||
title: t('Header:About'),
|
||||
icon: 'info',
|
||||
onClick: showAboutModal,
|
||||
},
|
||||
{
|
||||
title: t('Header:Preferences'),
|
||||
icon: 'settings',
|
||||
onClick: showPreferencesModal,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<IconButton
|
||||
variant="text"
|
||||
color="inherit"
|
||||
size="initial"
|
||||
className="text-primary-active"
|
||||
>
|
||||
<Icon name="settings" />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
variant="text"
|
||||
color="inherit"
|
||||
size="initial"
|
||||
className="text-primary-active"
|
||||
>
|
||||
<Icon name="chevron-down" />
|
||||
</IconButton>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</NavBar>
|
||||
);
|
||||
}
|
||||
|
||||
Header.propTypes = {
|
||||
children: PropTypes.any.isRequired,
|
||||
};
|
||||
|
||||
export default Header;
|
||||
@ -1,12 +1,13 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { SidePanel, ErrorBoundary } from '@ohif/ui';
|
||||
import Header from './Header.jsx';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { SidePanel, ErrorBoundary, UserPreferences, AboutModal, Header, useModal } from '@ohif/ui';
|
||||
|
||||
import NestedMenu from './ToolbarButtonNestedMenu.jsx';
|
||||
|
||||
// TODO: Having ToolbarPrimary and ToolbarSecondary is ugly, but
|
||||
// these are going to be unified shortly so this is good enough for now.
|
||||
function ToolbarPrimary({servicesManager}) {
|
||||
function ToolbarPrimary({ servicesManager }) {
|
||||
const { ToolBarService } = servicesManager.services;
|
||||
const defaultTool = {
|
||||
icon: 'tool-more-menu',
|
||||
@ -82,7 +83,7 @@ function ToolbarPrimary({servicesManager}) {
|
||||
</>
|
||||
}
|
||||
|
||||
function ToolbarSecondary({servicesManager}) {
|
||||
function ToolbarSecondary({ servicesManager }) {
|
||||
const { ToolBarService } = servicesManager.services;
|
||||
const defaultTool = {
|
||||
icon: 'tool-more-menu',
|
||||
@ -137,19 +138,46 @@ function ToolbarSecondary({servicesManager}) {
|
||||
</>
|
||||
}
|
||||
|
||||
|
||||
function ViewerLayout({
|
||||
// From Extension Module Params
|
||||
extensionManager,
|
||||
servicesManager,
|
||||
commandsManager,
|
||||
hotkeysManager,
|
||||
// From Modes
|
||||
leftPanels,
|
||||
rightPanels,
|
||||
viewports,
|
||||
children,
|
||||
ViewportGridComp,
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const { show, hide } = useModal();
|
||||
|
||||
const { hotkeyDefinitions, hotkeyDefaults } = hotkeysManager;
|
||||
const menuOptions = [
|
||||
{
|
||||
title: t('Header:About'),
|
||||
icon: 'info',
|
||||
onClick: () => show({ content: AboutModal, title: 'About OHIF Viewer' })
|
||||
},
|
||||
{
|
||||
title: t('Header:Preferences'),
|
||||
icon: 'settings',
|
||||
onClick: () => show({
|
||||
title: t('UserPreferencesModal:User Preferences'),
|
||||
content: UserPreferences,
|
||||
contentProps: {
|
||||
hotkeyDefaults: hotkeysManager.getValidHotkeyDefinitions(hotkeyDefaults),
|
||||
hotkeyDefinitions,
|
||||
onCancel: hide,
|
||||
onSubmit: ({ hotkeyDefinitions }) => {
|
||||
hotkeysManager.setHotkeys(hotkeyDefinitions);
|
||||
hide();
|
||||
},
|
||||
onReset: () => hotkeysManager.restoreDefaultBindings()
|
||||
}
|
||||
})
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Set body classes (tailwindcss) that don't allow vertical
|
||||
@ -194,10 +222,10 @@ function ViewerLayout({
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Header>
|
||||
<Header menuOptions={menuOptions}>
|
||||
<ErrorBoundary context="Primary Toolbar">
|
||||
<div className="relative flex justify-center">
|
||||
<ToolbarPrimary servicesManager={servicesManager}/>
|
||||
<ToolbarPrimary servicesManager={servicesManager} />
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
</Header>
|
||||
@ -220,7 +248,7 @@ function ViewerLayout({
|
||||
<div className="flex h-12 border-b border-transparent flex-2 w-100">
|
||||
<ErrorBoundary context="Secondary Toolbar">
|
||||
<div className="flex items-center w-full px-3 bg-primary-dark">
|
||||
<ToolbarSecondary servicesManager={servicesManager}/>
|
||||
<ToolbarSecondary servicesManager={servicesManager} />
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
|
||||
@ -5,16 +5,18 @@ import ViewerLayout from './ViewerLayout';
|
||||
- Init layout based on the displaySets and the objects.
|
||||
*/
|
||||
|
||||
export default function({
|
||||
export default function ({
|
||||
servicesManager,
|
||||
extensionManager,
|
||||
commandsManager,
|
||||
hotkeysManager
|
||||
}) {
|
||||
function ViewerLayoutWithServices(props) {
|
||||
return ViewerLayout({
|
||||
servicesManager,
|
||||
extensionManager,
|
||||
commandsManager,
|
||||
hotkeysManager,
|
||||
...props,
|
||||
});
|
||||
}
|
||||
|
||||
@ -5,6 +5,7 @@ const ohif = {
|
||||
layout: 'org.ohif.default.layoutTemplateModule.viewerLayout',
|
||||
sopClassHandler: 'org.ohif.default.sopClassHandlerModule.stack',
|
||||
};
|
||||
|
||||
const tracked = {
|
||||
measurements: 'org.ohif.measurement-tracking.panelModule.trackedMeasurements',
|
||||
thumbnailList: 'org.ohif.measurement-tracking.panelModule.seriesList',
|
||||
|
||||
@ -61,8 +61,7 @@ export class HotkeysManager {
|
||||
*/
|
||||
setHotkeys(hotkeyDefinitions = []) {
|
||||
try {
|
||||
const definitions = this._getValidDefinitions(hotkeyDefinitions);
|
||||
|
||||
const definitions = this.getValidDefinitions(hotkeyDefinitions);
|
||||
definitions.forEach(definition => this.registerHotkeys(definition));
|
||||
} catch (error) {
|
||||
const { UINotificationService } = this._servicesManager.services;
|
||||
@ -81,8 +80,7 @@ export class HotkeysManager {
|
||||
* @param {HotkeyDefinition[] | Object} [hotkeyDefinitions=[]] Contains hotkeys definitions
|
||||
*/
|
||||
setDefaultHotKeys(hotkeyDefinitions = []) {
|
||||
const definitions = this._getValidDefinitions(hotkeyDefinitions);
|
||||
|
||||
const definitions = this.getValidDefinitions(hotkeyDefinitions);
|
||||
this.hotkeyDefaults = definitions;
|
||||
}
|
||||
|
||||
@ -92,7 +90,7 @@ export class HotkeysManager {
|
||||
*
|
||||
* @param {HotkeyDefinition[] | Object} [hotkeyDefinitions=[]] Contains hotkeys definitions
|
||||
*/
|
||||
_getValidDefinitions(hotkeyDefinitions) {
|
||||
getValidDefinitions(hotkeyDefinitions) {
|
||||
const definitions = Array.isArray(hotkeyDefinitions)
|
||||
? [...hotkeyDefinitions]
|
||||
: this._parseToArrayLike(hotkeyDefinitions);
|
||||
@ -100,6 +98,24 @@ export class HotkeysManager {
|
||||
return definitions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Take hotkey definitions that can be an array and make sure that it
|
||||
* returns an object of hotkeys definitions
|
||||
*
|
||||
* @param {HotkeyDefinition[]} [hotkeyDefinitions=[]] Contains hotkeys definitions
|
||||
* @returns {Object}
|
||||
*/
|
||||
getValidHotkeyDefinitions(hotkeyDefinitions) {
|
||||
const definitions = this.getValidDefinitions(hotkeyDefinitions);
|
||||
const objectDefinitions = {};
|
||||
definitions.forEach(definition => {
|
||||
const { commandName, commandOptions } = definition;
|
||||
const commandHash = objectHash({ commandName, commandOptions });
|
||||
objectDefinitions[commandHash] = definition;
|
||||
});
|
||||
return objectDefinitions;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
@ -147,7 +163,7 @@ export class HotkeysManager {
|
||||
* @param {String} extension
|
||||
* @returns {undefined}
|
||||
*/
|
||||
registerHotkeys({ commandName, commandOptions = {}, keys, label } = {}, extension) {
|
||||
registerHotkeys({ commandName, commandOptions = {}, keys, label, isEditable } = {}, extension) {
|
||||
if (!commandName) {
|
||||
log.warn(`[hotkeys] No command was defined for hotkey "${keys}"`);
|
||||
return;
|
||||
@ -164,7 +180,7 @@ export class HotkeysManager {
|
||||
}
|
||||
|
||||
// Set definition & bind
|
||||
this.hotkeyDefinitions[commandHash] = { keys, label };
|
||||
this.hotkeyDefinitions[commandHash] = { commandName, commandOptions, keys, label, isEditable };
|
||||
this._bindHotkeys(commandName, commandOptions, keys);
|
||||
log.info(`[hotkeys] Binding ${commandName} with ${options} options to ${keys}`);
|
||||
}
|
||||
|
||||
@ -4,53 +4,64 @@ import windowLevelPresets from './windowLevelPresets';
|
||||
* Supported Keys: https://craig.is/killing/mice
|
||||
*/
|
||||
export default [
|
||||
/** Global */
|
||||
{
|
||||
commandName: 'incrementActiveViewport',
|
||||
label: 'Next Viewport',
|
||||
keys: ['right'],
|
||||
},
|
||||
{
|
||||
commandName: 'decrementActiveViewport',
|
||||
label: 'Previous Viewport',
|
||||
keys: ['left'],
|
||||
},
|
||||
/** Viewport */
|
||||
{ commandName: 'rotateViewportCW', label: 'Rotate Right', keys: ['r'] },
|
||||
{ commandName: 'rotateViewportCCW', label: 'Rotate Left', keys: ['l'] },
|
||||
{ commandName: 'invertViewport', label: 'Invert', keys: ['i'] },
|
||||
{
|
||||
commandName: 'cancelMeasurement',
|
||||
label: 'Cancel Cornerstone Measurement',
|
||||
keys: ['esc'],
|
||||
},
|
||||
{ commandName: 'setToolActive', commandOptions: { toolName: 'Zoom' }, label: 'Zoom', keys: ['z'], isEditable: true },
|
||||
{ commandName: 'scaleUpViewport', label: 'Zoom In', keys: ['+'], isEditable: true },
|
||||
{ commandName: 'scaleDownViewport', label: 'Zoom Out', keys: ['-'], isEditable: true },
|
||||
{ commandName: 'fitViewportToWindow', label: 'Zoom to Fit', keys: ['='], isEditable: true },
|
||||
{ commandName: 'rotateViewportCW', label: 'Rotate Right', keys: ['r'], isEditable: true },
|
||||
{ commandName: 'rotateViewportCCW', label: 'Rotate Left', keys: ['l'], isEditable: true },
|
||||
{
|
||||
commandName: 'flipViewportVertical',
|
||||
label: 'Flip Horizontally',
|
||||
keys: ['h'],
|
||||
isEditable: true,
|
||||
},
|
||||
{
|
||||
commandName: 'flipViewportHorizontal',
|
||||
label: 'Flip Vertically',
|
||||
keys: ['v'],
|
||||
isEditable: true,
|
||||
},
|
||||
{ commandName: 'scaleUpViewport', label: 'Zoom In', keys: ['+'] },
|
||||
{ commandName: 'scaleDownViewport', label: 'Zoom Out', keys: ['-'] },
|
||||
{ commandName: 'fitViewportToWindow', label: 'Zoom to Fit', keys: ['='] },
|
||||
{ commandName: 'resetViewport', label: 'Reset', keys: ['space'] },
|
||||
{ commandName: 'nextImage', label: 'Next Image', keys: ['down'] },
|
||||
{ commandName: 'previousImage', label: 'Previous Image', keys: ['up'] },
|
||||
{
|
||||
commandName: 'previousViewportDisplaySet',
|
||||
label: 'Previous Series',
|
||||
keys: ['pagedown'],
|
||||
commandName: 'invertViewport',
|
||||
label: 'Invert',
|
||||
keys: ['i'],
|
||||
isEditable: true,
|
||||
},
|
||||
{
|
||||
commandName: 'incrementActiveViewport',
|
||||
label: 'Next Image Viewport',
|
||||
keys: ['right'],
|
||||
isEditable: true,
|
||||
},
|
||||
{
|
||||
commandName: 'decrementActiveViewport',
|
||||
label: 'Previous Image Viewport',
|
||||
keys: ['left'],
|
||||
isEditable: true,
|
||||
},
|
||||
{
|
||||
commandName: 'nextViewportDisplaySet',
|
||||
label: 'Next Series',
|
||||
keys: ['pageup'],
|
||||
isEditable: true,
|
||||
},
|
||||
{
|
||||
commandName: 'previousViewportDisplaySet',
|
||||
label: 'Previous Series',
|
||||
keys: ['pagedown'],
|
||||
isEditable: true,
|
||||
},
|
||||
{ commandName: 'nextImage', label: 'Next Image', keys: ['down'], isEditable: true },
|
||||
{ commandName: 'previousImage', label: 'Previous Image', keys: ['up'], isEditable: true },
|
||||
{ commandName: 'firstImage', label: 'First Image', keys: ['home'], isEditable: true },
|
||||
{ commandName: 'lastImage', label: 'Last Image', keys: ['end'], isEditable: true },
|
||||
{ commandName: 'resetViewport', label: 'Reset', keys: ['space'], isEditable: true },
|
||||
{
|
||||
commandName: 'cancelMeasurement',
|
||||
label: 'Cancel Cornerstone Measurement',
|
||||
keys: ['esc'],
|
||||
},
|
||||
/** Window level presets */
|
||||
{
|
||||
commandName: 'setWindowLevel',
|
||||
commandOptions: windowLevelPresets[1],
|
||||
|
||||
@ -5,8 +5,8 @@ export default {
|
||||
4: { description: 'Bone', window: '80', level: '40' },
|
||||
5: { description: 'Brain', window: '2500', level: '480' },
|
||||
6: { description: 'Trest', window: '1', level: '1' },
|
||||
7: { description: '', window: '', level: '' },
|
||||
8: { description: '', window: '', level: '' },
|
||||
9: { description: '', window: '', level: '' },
|
||||
10: { description: '', window: '', level: '' },
|
||||
7: { description: 'Empty1', window: 'Empty1', level: 'Empty1' },
|
||||
8: { description: 'Empty2', window: 'Empty2', level: 'Empty2' },
|
||||
9: { description: 'Empty3', window: 'Empty3', level: 'Empty3' },
|
||||
10: { description: 'Empty4', window: 'Empty4', level: 'Empty4' },
|
||||
};
|
||||
|
||||
@ -2,13 +2,14 @@ import MODULE_TYPES from './MODULE_TYPES.js';
|
||||
import log from './../log.js';
|
||||
|
||||
export default class ExtensionManager {
|
||||
constructor({ commandsManager, servicesManager, api, appConfig = {} }) {
|
||||
constructor({ commandsManager, servicesManager, hotkeysManager, api, appConfig = {} }) {
|
||||
this.modules = {};
|
||||
this.registeredExtensionIds = [];
|
||||
this.moduleTypeNames = Object.values(MODULE_TYPES);
|
||||
//
|
||||
this._commandsManager = commandsManager;
|
||||
this._servicesManager = servicesManager;
|
||||
this._hotkeysManager = hotkeysManager;
|
||||
this._appConfig = appConfig;
|
||||
this._api = api;
|
||||
|
||||
@ -31,6 +32,7 @@ export default class ExtensionManager {
|
||||
registeredExtensionIds,
|
||||
_servicesManager,
|
||||
_commandsManager,
|
||||
_hotkeysManager,
|
||||
_extensionLifeCycleHooks,
|
||||
} = this;
|
||||
|
||||
@ -51,6 +53,7 @@ export default class ExtensionManager {
|
||||
onModeEnter({
|
||||
servicesManager: _servicesManager,
|
||||
commandsManager: _commandsManager,
|
||||
hotkeysManager: _hotkeysManager
|
||||
});
|
||||
}
|
||||
});
|
||||
@ -139,6 +142,7 @@ export default class ExtensionManager {
|
||||
extension.preRegistration({
|
||||
servicesManager: this._servicesManager,
|
||||
commandsManager: this._commandsManager,
|
||||
hotkeysManager: this._hotkeysManager,
|
||||
appConfig: this._appConfig,
|
||||
configuration,
|
||||
});
|
||||
@ -239,8 +243,9 @@ export default class ExtensionManager {
|
||||
appConfig: this._appConfig,
|
||||
getDataSources: this.getDataSources, // Why pass this in if we're passing in `extensionManager`?
|
||||
commandsManager: this._commandsManager,
|
||||
extensionManager: this,
|
||||
servicesManager: this._servicesManager,
|
||||
hotkeysManager: this._hotkeysManager,
|
||||
extensionManager: this,
|
||||
configuration,
|
||||
api: this._api,
|
||||
});
|
||||
|
||||
@ -7,6 +7,7 @@ import { initReactI18next } from 'react-i18next';
|
||||
import customDebug from './debugger';
|
||||
import pkg from '../package.json';
|
||||
import { debugMode, detectionOptions } from './config';
|
||||
import { getLanguageLabel, getAvailableLanguagesInfo } from './utils.js';
|
||||
|
||||
// Note: The index.js files inside src/locales are dynamically generated
|
||||
// by the pullTranslations.sh script
|
||||
@ -127,7 +128,7 @@ function initI18n(
|
||||
});
|
||||
}
|
||||
|
||||
return initialized.then(function(t) {
|
||||
return initialized.then(function (t) {
|
||||
i18n.T = t;
|
||||
customDebug(`T function available.`, 'info');
|
||||
});
|
||||
@ -138,9 +139,8 @@ customDebug(`version ${pkg.version} loaded.`, 'info');
|
||||
i18n.initializing = initI18n();
|
||||
i18n.initI18n = initI18n;
|
||||
i18n.addLocales = addLocales;
|
||||
i18n.defaultLanguage = DEFAULT_LANGUAGE;
|
||||
|
||||
import getAvailableLanguagesInfo from './getAvailableLanguagesInfo.js';
|
||||
i18n.availableLanguages = getAvailableLanguagesInfo(locales);
|
||||
i18n.defaultLanguage = { label: getLanguageLabel(DEFAULT_LANGUAGE), value: DEFAULT_LANGUAGE };
|
||||
i18n.currentLanguage = () => ({ label: getLanguageLabel(i18n.language), value: i18n.language });
|
||||
|
||||
export default i18n;
|
||||
|
||||
@ -56,15 +56,21 @@ const languagesMap = {
|
||||
'zh-TW': 'Chinese (Taiwan)',
|
||||
};
|
||||
|
||||
const getLanguageLabel = (language) => {
|
||||
return languagesMap[language];
|
||||
};
|
||||
|
||||
export default function getAvailableLanguagesInfo(locales) {
|
||||
const availableLanguagesInfo = [];
|
||||
|
||||
Object.keys(locales).forEach(key => {
|
||||
availableLanguagesInfo.push({
|
||||
value: key,
|
||||
label: languagesMap[key] || key,
|
||||
label: getLanguageLabel(key) || key,
|
||||
});
|
||||
});
|
||||
|
||||
return availableLanguagesInfo;
|
||||
}
|
||||
|
||||
export { getAvailableLanguagesInfo, getLanguageLabel };
|
||||
@ -30,6 +30,11 @@ export {
|
||||
|
||||
/** COMPONENTS */
|
||||
export {
|
||||
AboutModal,
|
||||
HotkeyField,
|
||||
Header,
|
||||
UserPreferences,
|
||||
HotkeysPreferences,
|
||||
Button,
|
||||
ButtonGroup,
|
||||
ContextMenu,
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { Typography, Icon } from '@ohif/ui';
|
||||
import { Typography, Icon } from '../../components';
|
||||
import detect from 'browser-detect';
|
||||
|
||||
const Link = ({ href, children, showIcon = false }) => {
|
||||
@ -29,14 +29,14 @@ const Row = ({ title, value, link }) => {
|
||||
{link ? (
|
||||
<Link href={link}>{value}</Link>
|
||||
) : (
|
||||
<Typography
|
||||
variant="subtitle"
|
||||
component="p"
|
||||
className="text-white w-48"
|
||||
>
|
||||
{value}
|
||||
</Typography>
|
||||
)}
|
||||
<Typography
|
||||
variant="subtitle"
|
||||
component="p"
|
||||
className="text-white w-48"
|
||||
>
|
||||
{value}
|
||||
</Typography>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
30
platform/ui/src/components/AboutModal/AboutModal.mdx
Normal file
30
platform/ui/src/components/AboutModal/AboutModal.mdx
Normal file
@ -0,0 +1,30 @@
|
||||
---
|
||||
name: AboutModal
|
||||
menu: General
|
||||
route: components/aboutModal
|
||||
---
|
||||
|
||||
import { Playground, Props } from 'docz';
|
||||
import { AboutModal } from '@ohif/ui';
|
||||
|
||||
# AboutModal
|
||||
|
||||
AboutModal are used to show application version information.
|
||||
|
||||
## Import
|
||||
|
||||
```javascript
|
||||
import { AboutModal } from '@ohif/ui';
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
<Playground>
|
||||
<div className="p-4">
|
||||
<AboutModal />
|
||||
</div>
|
||||
</Playground>
|
||||
|
||||
## Properties
|
||||
|
||||
<Props of={AboutModal} />
|
||||
2
platform/ui/src/components/AboutModal/index.js
Normal file
2
platform/ui/src/components/AboutModal/index.js
Normal file
@ -0,0 +1,2 @@
|
||||
import AboutModal from './AboutModal';
|
||||
export default AboutModal;
|
||||
@ -64,6 +64,8 @@ const variantClasses = {
|
||||
'bg-white text-black hover:opacity-80 active:opacity-100 focus:opacity-80',
|
||||
black:
|
||||
'bg-black text-white hover:opacity-80 active:opacity-100 focus:opacity-80',
|
||||
light:
|
||||
'border bg-primary-light border-primary-light text-black hover:opacity-80 active:opacity-100 focus:opacity-80',
|
||||
},
|
||||
};
|
||||
|
||||
@ -154,6 +156,7 @@ Button.propTypes = {
|
||||
'white',
|
||||
'black',
|
||||
'inherit',
|
||||
'light'
|
||||
]),
|
||||
fullWidth: PropTypes.bool,
|
||||
disabled: PropTypes.bool,
|
||||
|
||||
71
platform/ui/src/components/Header/Header.jsx
Normal file
71
platform/ui/src/components/Header/Header.jsx
Normal file
@ -0,0 +1,71 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import classNames from 'classnames';
|
||||
// TODO: This may fail if package is split from PWA build
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { NavBar, Svg, Icon, IconButton, Dropdown } from '@ohif/ui';
|
||||
|
||||
function Header({ children, menuOptions, isReturnEnabled }) {
|
||||
const { t } = useTranslation();
|
||||
const history = useHistory();
|
||||
|
||||
const onReturnHandler = () => {
|
||||
if (isReturnEnabled) {
|
||||
history.push('/');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<NavBar className="justify-between border-b-4 border-black">
|
||||
<div className="flex justify-between flex-1">
|
||||
<div className="flex items-center">
|
||||
{/* // TODO: Should preserve filter/sort
|
||||
// Either injected service? Or context (like react router's `useLocation`?) */}
|
||||
<div
|
||||
className={classNames("inline-flex items-center mr-3", isReturnEnabled && 'cursor-pointer')}
|
||||
onClick={onReturnHandler}
|
||||
>
|
||||
{isReturnEnabled && <Icon name="chevron-left" className="w-8 text-primary-active" />}
|
||||
<div className="ml-4"><Svg name="logo-ohif" /></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center">{children}</div>
|
||||
<div className="flex items-center">
|
||||
<span className="mr-3 text-lg text-common-light">
|
||||
{t('Header:INVESTIGATIONAL USE ONLY')}
|
||||
</span>
|
||||
<Dropdown showDropdownIcon={false} list={menuOptions}>
|
||||
<IconButton
|
||||
variant="text"
|
||||
color="inherit"
|
||||
size="initial"
|
||||
className="text-primary-active"
|
||||
>
|
||||
<Icon name="settings" />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
variant="text"
|
||||
color="inherit"
|
||||
size="initial"
|
||||
className="text-primary-active"
|
||||
>
|
||||
<Icon name="chevron-down" />
|
||||
</IconButton>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</NavBar>
|
||||
);
|
||||
}
|
||||
|
||||
Header.propTypes = {
|
||||
children: PropTypes.oneOfType([PropTypes.node, PropTypes.func]),
|
||||
isReturnEnabled: PropTypes.bool
|
||||
};
|
||||
|
||||
Header.defaultProps = {
|
||||
isReturnEnabled: true
|
||||
};
|
||||
|
||||
export default Header;
|
||||
2
platform/ui/src/components/Header/index.js
Normal file
2
platform/ui/src/components/Header/index.js
Normal file
@ -0,0 +1,2 @@
|
||||
import Header from './Header';
|
||||
export default Header;
|
||||
63
platform/ui/src/components/HotkeyField/HotkeyField.jsx
Normal file
63
platform/ui/src/components/HotkeyField/HotkeyField.jsx
Normal file
@ -0,0 +1,63 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { hotkeys } from '@ohif/core';
|
||||
import { Input } from '@ohif/ui';
|
||||
|
||||
import { getKeys, formatKeysForInput } from './utils';
|
||||
|
||||
/**
|
||||
* HotkeyField
|
||||
* Renders a hotkey input that records keys
|
||||
*
|
||||
* @param {object} props component props
|
||||
* @param {Array[]} props.keys keys to be controlled by this field
|
||||
* @param {boolean} props.disabled disables the field
|
||||
* @param {function} props.onChange callback with changed values
|
||||
* @param {string} props.className input classes
|
||||
* @param {Array[]} props.modifierKeys
|
||||
*/
|
||||
const HotkeyField = ({ disabled, keys, onChange, className, modifierKeys }) => {
|
||||
const inputValue = formatKeysForInput(keys);
|
||||
|
||||
const onInputKeyDown = event => {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
|
||||
hotkeys.record(sequence => {
|
||||
const keys = getKeys({ sequence, modifierKeys });
|
||||
hotkeys.unpause();
|
||||
onChange(keys);
|
||||
});
|
||||
};
|
||||
|
||||
const onFocus = () => {
|
||||
hotkeys.pause();
|
||||
hotkeys.startRecording();
|
||||
};
|
||||
|
||||
return (
|
||||
<Input
|
||||
readOnly
|
||||
disabled={disabled}
|
||||
value={inputValue}
|
||||
onKeyDown={onInputKeyDown}
|
||||
onFocus={onFocus}
|
||||
className={className}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
HotkeyField.propTypes = {
|
||||
keys: PropTypes.array.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
className: PropTypes.string,
|
||||
modifierKeys: PropTypes.array,
|
||||
disabled: PropTypes.bool,
|
||||
};
|
||||
|
||||
HotkeyField.defaultProps = {
|
||||
disabled: false
|
||||
};
|
||||
|
||||
export default HotkeyField;
|
||||
3
platform/ui/src/components/HotkeyField/index.js
Normal file
3
platform/ui/src/components/HotkeyField/index.js
Normal file
@ -0,0 +1,3 @@
|
||||
import HotkeyField from './HotkeyField.jsx';
|
||||
|
||||
export default HotkeyField;
|
||||
31
platform/ui/src/components/HotkeyField/utils.js
Normal file
31
platform/ui/src/components/HotkeyField/utils.js
Normal file
@ -0,0 +1,31 @@
|
||||
/**
|
||||
* Take the pressed key array and return the readable string for the keys
|
||||
*
|
||||
* @param {Array} [keys=[]]
|
||||
* @returns {string} string representation of an array of keys
|
||||
*/
|
||||
const formatKeysForInput = (keys = []) => keys.join('+');
|
||||
|
||||
/**
|
||||
* formats given keys sequence to insert the modifier keys in the first index of the array
|
||||
* @param {string} sequence keys sequence from MouseTrap Record -> "shift+left"
|
||||
* @returns {Array} keys in array-format -> ['shift','left']
|
||||
*/
|
||||
const getKeys = ({ sequence, modifierKeys }) => {
|
||||
const keysArray = sequence.join(' ').split('+');
|
||||
let keys = [];
|
||||
let modifiers = [];
|
||||
keysArray.forEach(key => {
|
||||
if (modifierKeys && modifierKeys.includes(key)) {
|
||||
modifiers.push(key);
|
||||
} else {
|
||||
keys.push(key);
|
||||
}
|
||||
});
|
||||
return [...modifiers, ...keys];
|
||||
};
|
||||
|
||||
export {
|
||||
getKeys,
|
||||
formatKeysForInput
|
||||
};
|
||||
@ -0,0 +1,111 @@
|
||||
import React, { useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import { HotkeyField, Typography } from '@ohif/ui';
|
||||
|
||||
/* TODO: Move these configs and utils to core? */
|
||||
import { MODIFIER_KEYS } from './hotkeysConfig';
|
||||
import { validate, splitHotkeyDefinitionsAndCreateTuples } from './utils';
|
||||
|
||||
const HotkeysPreferences = ({ disabled, hotkeyDefinitions, errors: controlledErrors, onChange }) => {
|
||||
const visibleHotkeys = Object.keys(hotkeyDefinitions)
|
||||
.filter(key => hotkeyDefinitions[key].isEditable)
|
||||
.reduce((obj, key) => {
|
||||
obj[key] = hotkeyDefinitions[key];
|
||||
return obj;
|
||||
}, {});
|
||||
|
||||
const [errors, setErrors] = useState(controlledErrors);
|
||||
const splitedHotkeys = splitHotkeyDefinitionsAndCreateTuples(visibleHotkeys);
|
||||
|
||||
if (!Object.keys(hotkeyDefinitions).length) {
|
||||
return 'No hotkeys definitions';
|
||||
}
|
||||
|
||||
const onHotkeyChangeHandler = (id, definition) => {
|
||||
const { error } = validate({
|
||||
commandName: id,
|
||||
pressedKeys: definition.keys,
|
||||
hotkeys: hotkeyDefinitions,
|
||||
});
|
||||
|
||||
setErrors(prevState => {
|
||||
const errors = { ...prevState, [id]: error };
|
||||
onChange(id, definition, errors);
|
||||
return errors;
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='flex flex-row justify-center'>
|
||||
<div className='flex flex-row justify-evenly w-full'>
|
||||
{splitedHotkeys.map((hotkeys, index) => {
|
||||
return (
|
||||
<div key={`HotkeyGroup@${index}`} className='flex flex-row'>
|
||||
<div className='p-2 text-right flex flex-col'>
|
||||
{hotkeys.map((hotkey, hotkeyIndex) => {
|
||||
const [id, definition] = hotkey;
|
||||
const isFirst = hotkeyIndex === 0;
|
||||
const error = errors[id];
|
||||
|
||||
const onChangeHandler = keys => onHotkeyChangeHandler(id, { ...definition, keys });
|
||||
|
||||
return (
|
||||
<div key={`HotkeyItem@${hotkeyIndex}`} className='flex flex-row justify-end mb-2'>
|
||||
<div className='flex flex-col items-center'>
|
||||
<Typography
|
||||
variant='subtitle'
|
||||
className={classNames('pr-6 w-full text-right text-primary-light', !isFirst && 'hidden')}
|
||||
>
|
||||
Function
|
||||
</Typography>
|
||||
<Typography
|
||||
variant='subtitle'
|
||||
className={classNames('pr-6 h-full flex flex-row items-center whitespace-no-wrap', isFirst && 'mt-5')}>
|
||||
{definition.label}
|
||||
</Typography>
|
||||
</div>
|
||||
<div className='flex flex-col'>
|
||||
<Typography
|
||||
variant='subtitle'
|
||||
className={classNames('pr-6 pl-0 text-left text-primary-light', !isFirst && 'hidden')}
|
||||
>
|
||||
Shortcut
|
||||
</Typography>
|
||||
<div className={classNames('flex flex-col w-32', isFirst && 'mt-5')}>
|
||||
<HotkeyField
|
||||
disabled={disabled}
|
||||
keys={definition.keys}
|
||||
modifierKeys={MODIFIER_KEYS}
|
||||
onChange={onChangeHandler}
|
||||
className='text-lg h-8'
|
||||
/>
|
||||
{error && <span className='p-2 text-red-600 text-sm'>{error}</span>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const noop = () => { };
|
||||
|
||||
HotkeysPreferences.propTypes = {
|
||||
onChange: PropTypes.func,
|
||||
disabled: PropTypes.bool,
|
||||
hotkeyDefinitions: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
HotkeysPreferences.defaultProps = {
|
||||
onChange: noop,
|
||||
disabled: false
|
||||
};
|
||||
|
||||
export default HotkeysPreferences;
|
||||
@ -0,0 +1,91 @@
|
||||
const range = (start, end) => {
|
||||
return new Array(end - start).fill().map((d, i) => i + start);
|
||||
};
|
||||
|
||||
export const MODIFIER_KEYS = ['ctrl', 'alt', 'shift'];
|
||||
|
||||
export const DISALLOWED_COMBINATIONS = {
|
||||
'': [],
|
||||
alt: ['space'],
|
||||
shift: [],
|
||||
ctrl: [
|
||||
'f4',
|
||||
'f5',
|
||||
'f11',
|
||||
'w',
|
||||
'r',
|
||||
't',
|
||||
'o',
|
||||
'p',
|
||||
'a',
|
||||
'd',
|
||||
'f',
|
||||
'g',
|
||||
'h',
|
||||
'j',
|
||||
'l',
|
||||
'z',
|
||||
'x',
|
||||
'c',
|
||||
'v',
|
||||
'b',
|
||||
'n',
|
||||
'pagedown',
|
||||
'pageup',
|
||||
],
|
||||
'ctrl+shift': ['q', 'w', 'r', 't', 'p', 'a', 'h', 'v', 'b', 'n'],
|
||||
};
|
||||
|
||||
export const SPECIAL_KEYS = {
|
||||
8: 'backspace',
|
||||
9: 'tab',
|
||||
13: 'return',
|
||||
16: 'shift',
|
||||
17: 'ctrl',
|
||||
18: 'alt',
|
||||
19: 'pause',
|
||||
20: 'capslock',
|
||||
27: 'esc',
|
||||
32: 'space',
|
||||
33: 'pageup',
|
||||
34: 'pagedown',
|
||||
35: 'end',
|
||||
36: 'home',
|
||||
37: 'left',
|
||||
38: 'up',
|
||||
39: 'right',
|
||||
40: 'down',
|
||||
45: 'insert',
|
||||
46: 'del',
|
||||
96: '0',
|
||||
97: '1',
|
||||
98: '2',
|
||||
99: '3',
|
||||
100: '4',
|
||||
101: '5',
|
||||
102: '6',
|
||||
103: '7',
|
||||
104: '8',
|
||||
105: '9',
|
||||
106: '*',
|
||||
107: '+',
|
||||
109: '-',
|
||||
110: '.',
|
||||
111: '/',
|
||||
112: 'f1',
|
||||
113: 'f2',
|
||||
114: 'f3',
|
||||
115: 'f4',
|
||||
116: 'f5',
|
||||
117: 'f6',
|
||||
118: 'f7',
|
||||
119: 'f8',
|
||||
120: 'f9',
|
||||
121: 'f10',
|
||||
122: 'f11',
|
||||
123: 'f12',
|
||||
144: 'numlock',
|
||||
145: 'scroll',
|
||||
191: '/',
|
||||
224: 'meta',
|
||||
};
|
||||
@ -0,0 +1,87 @@
|
||||
import { MODIFIER_KEYS, DISALLOWED_COMBINATIONS } from './hotkeysConfig';
|
||||
|
||||
const formatPressedKeys = pressedKeysArray => pressedKeysArray.join('+');
|
||||
|
||||
const findConflictingCommand = (hotkeys, currentCommandName, pressedKeys) => {
|
||||
let firstConflictingCommand = undefined;
|
||||
const formatedPressedHotkeys = formatPressedKeys(pressedKeys);
|
||||
|
||||
for (const commandName in hotkeys) {
|
||||
const toolHotkeys = hotkeys[commandName].keys;
|
||||
const formatedToolHotkeys = formatPressedKeys(toolHotkeys);
|
||||
|
||||
if (
|
||||
formatedPressedHotkeys === formatedToolHotkeys &&
|
||||
commandName !== currentCommandName
|
||||
) {
|
||||
firstConflictingCommand = hotkeys[commandName];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return firstConflictingCommand;
|
||||
};
|
||||
|
||||
const ERROR_MESSAGES = {
|
||||
MODIFIER:
|
||||
"It's not possible to define only modifier keys (ctrl, alt and shift) as a shortcut",
|
||||
EMPTY: "Field can't be empty.",
|
||||
};
|
||||
|
||||
// VALIDATORS
|
||||
|
||||
const modifierValidator = ({ pressedKeys }) => {
|
||||
const lastPressedKey = pressedKeys[pressedKeys.length - 1];
|
||||
// Check if it has a valid modifier
|
||||
const isModifier = MODIFIER_KEYS.includes(lastPressedKey);
|
||||
if (isModifier) {
|
||||
return { error: ERROR_MESSAGES.MODIFIER };
|
||||
}
|
||||
};
|
||||
|
||||
const emptyValidator = ({ pressedKeys = [] }) => {
|
||||
if (!pressedKeys.length) {
|
||||
return { error: ERROR_MESSAGES.EMPTY };
|
||||
}
|
||||
};
|
||||
|
||||
const conflictingValidator = ({ commandName, pressedKeys, hotkeys }) => {
|
||||
const conflictingCommand = findConflictingCommand(
|
||||
hotkeys,
|
||||
commandName,
|
||||
pressedKeys
|
||||
);
|
||||
|
||||
if (conflictingCommand) {
|
||||
return {
|
||||
error: `"${conflictingCommand.label}" is already using the "${pressedKeys}" shortcut.`,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const disallowedValidator = ({ pressedKeys = [] }) => {
|
||||
const lastPressedKey = pressedKeys[pressedKeys.length - 1];
|
||||
const modifierCommand = formatPressedKeys(
|
||||
pressedKeys.slice(0, pressedKeys.length - 1)
|
||||
);
|
||||
|
||||
const disallowedCombination = DISALLOWED_COMBINATIONS[modifierCommand];
|
||||
const hasDisallowedCombinations = disallowedCombination
|
||||
? disallowedCombination.includes(lastPressedKey)
|
||||
: false;
|
||||
|
||||
if (hasDisallowedCombinations) {
|
||||
return {
|
||||
error: `"${formatPressedKeys(pressedKeys)}" shortcut combination is not allowed`,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const hotkeysValidators = [
|
||||
emptyValidator,
|
||||
modifierValidator,
|
||||
conflictingValidator,
|
||||
disallowedValidator,
|
||||
];
|
||||
|
||||
export { hotkeysValidators };
|
||||
3
platform/ui/src/components/HotkeysPreferences/index.js
Normal file
3
platform/ui/src/components/HotkeysPreferences/index.js
Normal file
@ -0,0 +1,3 @@
|
||||
import HotkeysPreferences from './HotkeysPreferences.jsx';
|
||||
|
||||
export default HotkeysPreferences;
|
||||
52
platform/ui/src/components/HotkeysPreferences/utils.js
Normal file
52
platform/ui/src/components/HotkeysPreferences/utils.js
Normal file
@ -0,0 +1,52 @@
|
||||
import { hotkeysValidators } from './hotkeysValidators';
|
||||
|
||||
/**
|
||||
* Split hotkeys definitions and create hotkey related tuples
|
||||
*
|
||||
* @param {array} hotkeyDefinitions
|
||||
* @returns {array} array of tuples consisted of command name and hotkey definition
|
||||
*/
|
||||
const splitHotkeyDefinitionsAndCreateTuples = hotkeyDefinitions => {
|
||||
const splitedHotkeys = [];
|
||||
const arrayHotkeys = Object.entries(hotkeyDefinitions);
|
||||
|
||||
if (arrayHotkeys.length) {
|
||||
const halfwayThrough = Math.ceil(arrayHotkeys.length / 2);
|
||||
splitedHotkeys.push(arrayHotkeys.slice(0, halfwayThrough));
|
||||
splitedHotkeys.push(
|
||||
arrayHotkeys.slice(halfwayThrough, arrayHotkeys.length)
|
||||
);
|
||||
}
|
||||
|
||||
return splitedHotkeys;
|
||||
};
|
||||
|
||||
/**
|
||||
* Validate a hotkey change
|
||||
*
|
||||
* @param {Object} arguments
|
||||
* @param {string} arguments.commandName command name or id
|
||||
* @param {array} arguments.pressedKeys new keys
|
||||
* @param {array} arguments.hotkeys current hotkeys
|
||||
* @returns {Object} {error} validation error
|
||||
*/
|
||||
const validate = ({ commandName, pressedKeys, hotkeys }) => {
|
||||
for (const validator of hotkeysValidators) {
|
||||
const validation = validator({
|
||||
commandName,
|
||||
pressedKeys,
|
||||
hotkeys,
|
||||
});
|
||||
|
||||
if (validation && validation.error) {
|
||||
return validation;
|
||||
}
|
||||
}
|
||||
|
||||
return { error: undefined };
|
||||
};
|
||||
|
||||
export {
|
||||
validate,
|
||||
splitHotkeyDefinitionsAndCreateTuples
|
||||
};
|
||||
@ -4,7 +4,7 @@ import Label from '../Label';
|
||||
import classnames from 'classnames';
|
||||
|
||||
const baseInputClasses =
|
||||
'shadow transition duration-300 appearance-none border border-primary-main hover:border-gray-500 focus:border-gray-500 focus:outline-none rounded w-full py-2 px-3 mt-2 text-sm text-white leading-tight focus:outline-none';
|
||||
'shadow transition duration-300 appearance-none border border-primary-main hover:border-gray-500 focus:border-gray-500 focus:outline-none rounded w-full py-2 px-3 text-sm text-white leading-tight focus:outline-none';
|
||||
|
||||
const transparentClasses = {
|
||||
true: 'bg-transparent',
|
||||
@ -23,6 +23,9 @@ const Input = ({
|
||||
onFocus,
|
||||
autoFocus,
|
||||
onKeyPress,
|
||||
onKeyDown,
|
||||
readOnly,
|
||||
disabled,
|
||||
...otherProps
|
||||
}) => {
|
||||
return (
|
||||
@ -30,16 +33,21 @@ const Input = ({
|
||||
<Label className={labelClassName} text={label}></Label>
|
||||
<input
|
||||
className={classnames(
|
||||
label && 'mt-2',
|
||||
className,
|
||||
baseInputClasses,
|
||||
transparentClasses[transparent]
|
||||
transparentClasses[transparent],
|
||||
{ 'cursor-not-allowed': disabled }
|
||||
)}
|
||||
autoFocus
|
||||
disabled={disabled}
|
||||
readOnly={readOnly}
|
||||
autoFocus={autoFocus}
|
||||
type={type}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
onFocus={onFocus}
|
||||
onKeyPress={onKeyPress}
|
||||
onKeyDown={onKeyDown}
|
||||
{...otherProps}
|
||||
/>
|
||||
</div>
|
||||
@ -57,7 +65,10 @@ Input.propTypes = {
|
||||
onChange: PropTypes.func,
|
||||
onFocus: PropTypes.func,
|
||||
autoFocus: PropTypes.bool,
|
||||
readOnly: PropTypes.bool,
|
||||
onKeyPress: PropTypes.func,
|
||||
onKeyDown: PropTypes.func,
|
||||
disabled: PropTypes.bool
|
||||
};
|
||||
|
||||
export default Input;
|
||||
|
||||
@ -74,14 +74,14 @@ const Select = ({
|
||||
components={_components}
|
||||
placeholder={placeholder}
|
||||
options={options}
|
||||
value={selectedOptions}
|
||||
value={isMulti ? selectedOptions : value}
|
||||
onChange={(selectedOptions, { action }) => {
|
||||
const newSelection = !selectedOptions.length
|
||||
? selectedOptions
|
||||
: selectedOptions.reduce((acc, curr) => acc.concat([curr.value]), []);
|
||||
onChange(newSelection, action);
|
||||
}}
|
||||
></ReactSelect>
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@ -112,7 +112,7 @@ Select.propTypes = {
|
||||
})
|
||||
),
|
||||
placeholder: PropTypes.string,
|
||||
value: PropTypes.arrayOf(PropTypes.string),
|
||||
value: PropTypes.oneOfType(PropTypes.string, PropTypes.arrayOf(PropTypes.string)),
|
||||
};
|
||||
|
||||
export default Select;
|
||||
|
||||
144
platform/ui/src/components/UserPreferences/UserPreferences.jsx
Normal file
144
platform/ui/src/components/UserPreferences/UserPreferences.jsx
Normal file
@ -0,0 +1,144 @@
|
||||
import React, { useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Select, Typography, Button, HotkeysPreferences } from '@ohif/ui';
|
||||
import i18n from '@ohif/i18n';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
const { availableLanguages, defaultLanguage, currentLanguage } = i18n;
|
||||
|
||||
const UserPreferences = ({ disabled, hotkeyDefinitions, hotkeyDefaults, onCancel, onSubmit, onReset }) => {
|
||||
const { t } = useTranslation('UserPreferencesModal');
|
||||
const [state, setState] = useState({
|
||||
isDisabled: disabled,
|
||||
hotkeyErrors: {},
|
||||
hotkeyDefinitions,
|
||||
language: currentLanguage()
|
||||
});
|
||||
|
||||
const onSubmitHandler = () => {
|
||||
i18n.changeLanguage(state.language.value);
|
||||
onSubmit(state);
|
||||
};
|
||||
|
||||
const onResetHandler = () => {
|
||||
setState(state => ({
|
||||
...state,
|
||||
language: defaultLanguage,
|
||||
hotkeyDefinitions: hotkeyDefaults,
|
||||
hotkeyErrors: {},
|
||||
isDisabled: disabled,
|
||||
}));
|
||||
onReset();
|
||||
};
|
||||
|
||||
const onCancelHandler = () => {
|
||||
setState({ hotkeyDefinitions });
|
||||
onCancel();
|
||||
};
|
||||
|
||||
const onLanguageChangeHandler = (value) => {
|
||||
setState(state => ({ ...state, language: value }));
|
||||
};
|
||||
|
||||
const onHotkeysChangeHandler = (id, definition, errors) => {
|
||||
setState(state => ({
|
||||
...state,
|
||||
isDisabled: Object.values(errors).every(e => e !== undefined),
|
||||
hotkeyErrors: errors,
|
||||
hotkeyDefinitions: {
|
||||
...state.hotkeyDefinitions,
|
||||
[id]: definition,
|
||||
}
|
||||
}));
|
||||
};
|
||||
|
||||
const Section = ({ title, children }) => (
|
||||
<>
|
||||
<div className="border-b-2 border-black mb-2">
|
||||
<Typography
|
||||
variant="h5"
|
||||
className="flex flex-grow text-primary-light font-light pb-2"
|
||||
>
|
||||
{title}
|
||||
</Typography>
|
||||
</div>
|
||||
<div className="mt-4 mb-8">
|
||||
{children}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="p-2">
|
||||
<Section title="General">
|
||||
<div className="flex flex-row justify-center items-center w-72">
|
||||
<Typography variant="subtitle" className="mr-5 text-right h-full">
|
||||
Language
|
||||
</Typography>
|
||||
<Select
|
||||
isClearable={false}
|
||||
onChange={onLanguageChangeHandler}
|
||||
options={availableLanguages}
|
||||
value={state.language}
|
||||
/>
|
||||
</div>
|
||||
</Section>
|
||||
<Section title="Hotkeys">
|
||||
<HotkeysPreferences
|
||||
disabled={disabled}
|
||||
hotkeyDefinitions={state.hotkeyDefinitions}
|
||||
onChange={onHotkeysChangeHandler}
|
||||
errors={state.hotkeyErrors}
|
||||
/>
|
||||
</Section>
|
||||
<div className="flex flex-row justify-between">
|
||||
<Button variant="outlined" onClick={onResetHandler} disabled={disabled}>
|
||||
{t('Reset to Defaults')}
|
||||
</Button>
|
||||
<div className="flex flex-row">
|
||||
<Button variant="outlined" onClick={onCancelHandler}>
|
||||
{t('Cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
disabled={state.isDisabled}
|
||||
color="light"
|
||||
className="ml-2"
|
||||
onClick={onSubmitHandler}
|
||||
>
|
||||
{t('Save')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const noop = () => { };
|
||||
|
||||
UserPreferences.propTypes = {
|
||||
disabled: PropTypes.bool,
|
||||
hotkeyDefaults: PropTypes.object.isRequired,
|
||||
hotkeyDefinitions: PropTypes.object.isRequired,
|
||||
languageOptions: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
label: PropTypes.string.isRequired,
|
||||
value: PropTypes.any.isRequired,
|
||||
})
|
||||
),
|
||||
onCancel: PropTypes.func,
|
||||
onSubmit: PropTypes.func,
|
||||
onReset: PropTypes.func,
|
||||
};
|
||||
|
||||
UserPreferences.defaultProps = {
|
||||
languageOptions: [
|
||||
{ value: 'ONE', label: 'ONE' },
|
||||
{ value: 'TWO', label: 'TWO' },
|
||||
],
|
||||
onCancel: noop,
|
||||
onSubmit: noop,
|
||||
onReset: noop,
|
||||
disabled: true
|
||||
};
|
||||
|
||||
export default UserPreferences;
|
||||
3
platform/ui/src/components/UserPreferences/index.js
Normal file
3
platform/ui/src/components/UserPreferences/index.js
Normal file
@ -0,0 +1,3 @@
|
||||
import UserPreferences from './UserPreferences.jsx';
|
||||
|
||||
export default UserPreferences;
|
||||
@ -1,3 +1,4 @@
|
||||
import AboutModal from './AboutModal';
|
||||
import Button from './Button';
|
||||
import ButtonGroup from './ButtonGroup';
|
||||
import ContextMenu from './ContextMenu';
|
||||
@ -53,8 +54,17 @@ import ViewportActionBar from './ViewportActionBar';
|
||||
import ViewportDownloadForm from './ViewportDownloadForm';
|
||||
import ViewportGrid from './ViewportGrid';
|
||||
import ViewportPane from './ViewportPane';
|
||||
import UserPreferences from './UserPreferences';
|
||||
import HotkeysPreferences from './HotkeysPreferences';
|
||||
import HotkeyField from './HotkeyField';
|
||||
import Header from './Header';
|
||||
|
||||
export {
|
||||
AboutModal,
|
||||
HotkeyField,
|
||||
Header,
|
||||
UserPreferences,
|
||||
HotkeysPreferences,
|
||||
Button,
|
||||
ButtonGroup,
|
||||
ContextMenu,
|
||||
|
||||
@ -43,6 +43,7 @@ function appInit(appConfigOrFunc, defaultExtensions) {
|
||||
const extensionManager = new ExtensionManager({
|
||||
commandsManager,
|
||||
servicesManager,
|
||||
hotkeysManager,
|
||||
appConfig,
|
||||
});
|
||||
|
||||
|
||||
@ -1,56 +0,0 @@
|
||||
import React from 'react';
|
||||
import AboutModal from './AboutModal';
|
||||
import { Dropdown, IconButton, Icon, useModal } from '@ohif/ui';
|
||||
|
||||
const PreferencesDropdown = () => {
|
||||
const { show } = useModal();
|
||||
|
||||
const showAboutModal = () => {
|
||||
show({
|
||||
content: AboutModal,
|
||||
title: 'About OHIF Viewer',
|
||||
});
|
||||
};
|
||||
|
||||
const showPreferencesModal = () => {
|
||||
const modalComponent = () => <div>Preferences modal</div>;
|
||||
show({
|
||||
content: modalComponent,
|
||||
title: 'Preferences',
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
showDropdownIcon={false}
|
||||
list={[
|
||||
{ title: 'About', icon: 'info', onClick: showAboutModal },
|
||||
{
|
||||
title: 'Preferences',
|
||||
icon: 'settings',
|
||||
onClick: showPreferencesModal,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<IconButton
|
||||
variant="text"
|
||||
color="inherit"
|
||||
size="initial"
|
||||
className="text-primary-active"
|
||||
>
|
||||
<Icon name="settings" />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
variant="text"
|
||||
color="inherit"
|
||||
size="initial"
|
||||
className="text-primary-active"
|
||||
onClick={() => {}}
|
||||
>
|
||||
<Icon name="chevron-down" />
|
||||
</IconButton>
|
||||
</Dropdown>
|
||||
);
|
||||
};
|
||||
|
||||
export default PreferencesDropdown;
|
||||
@ -1 +0,0 @@
|
||||
export { default } from './PreferencesDropdown';
|
||||
@ -4,7 +4,6 @@ import PropTypes from 'prop-types';
|
||||
// TODO: DicomMetadataStore should be injected?
|
||||
import { DicomMetadataStore, utils } from '@ohif/core';
|
||||
import { DragAndDropProvider, ImageViewerProvider } from '@ohif/ui';
|
||||
//
|
||||
import { useQuery } from '@hooks';
|
||||
import ViewportGrid from '@components/ViewportGrid';
|
||||
import Compose from './Compose';
|
||||
@ -88,12 +87,10 @@ export default function ModeRoute({
|
||||
return;
|
||||
}
|
||||
|
||||
console.debug('[hotkeys] Setting up hotkeys...');
|
||||
hotkeysManager.setDefaultHotKeys(hotkeys);
|
||||
hotkeysManager.setHotkeys(hotkeys);
|
||||
|
||||
return () => {
|
||||
console.debug('[hotkeys] Removing hotkeys...');
|
||||
hotkeysManager.destroy();
|
||||
};
|
||||
}, []);
|
||||
@ -282,4 +279,5 @@ ModeRoute.propTypes = {
|
||||
dataSourceName: PropTypes.string,
|
||||
extensionManager: PropTypes.object,
|
||||
servicesManager: PropTypes.object,
|
||||
hotkeysManager: PropTypes.object,
|
||||
};
|
||||
|
||||
@ -5,25 +5,26 @@ import { Link } from 'react-router-dom';
|
||||
import moment from 'moment';
|
||||
import qs from 'query-string';
|
||||
import isEqual from 'lodash.isequal';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
//
|
||||
import filtersMeta from './filtersMeta.js';
|
||||
import { useAppConfig } from '@state';
|
||||
import { useDebounce, useQuery } from '@hooks';
|
||||
import { utils } from '@ohif/core';
|
||||
|
||||
import PreferencesDropdown from '../../components/PreferencesDropdown';
|
||||
|
||||
import {
|
||||
Icon,
|
||||
StudyListExpandedRow,
|
||||
Button,
|
||||
NavBar,
|
||||
Svg,
|
||||
EmptyStudies,
|
||||
StudyListTable,
|
||||
StudyListPagination,
|
||||
StudyListFilter,
|
||||
TooltipClipboard,
|
||||
Header,
|
||||
useModal,
|
||||
AboutModal,
|
||||
UserPreferences
|
||||
} from '@ohif/ui';
|
||||
|
||||
const seriesInStudiesMap = new Map();
|
||||
@ -32,7 +33,11 @@ const seriesInStudiesMap = new Map();
|
||||
* TODO:
|
||||
* - debounce `setFilterValues` (150ms?)
|
||||
*/
|
||||
function WorkList({ history, data: studies, isLoadingData, dataSource }) {
|
||||
function WorkList({ history, data: studies, isLoadingData, dataSource, hotkeysManager }) {
|
||||
const { hotkeyDefinitions, hotkeyDefaults } = hotkeysManager;
|
||||
const { show, hide } = useModal();
|
||||
const { t } = useTranslation();
|
||||
|
||||
// ~ Modes
|
||||
const [appConfig] = useAppConfig();
|
||||
// ~ Filters
|
||||
@ -340,25 +345,39 @@ function WorkList({ history, data: studies, isLoadingData, dataSource }) {
|
||||
|
||||
const hasStudies = numOfStudies > 0;
|
||||
|
||||
const menuOptions = [
|
||||
{
|
||||
title: t('Header:About'),
|
||||
icon: 'info',
|
||||
onClick: () => show({ content: AboutModal, title: 'About OHIF Viewer' })
|
||||
},
|
||||
{
|
||||
title: t('Header:Preferences'),
|
||||
icon: 'settings',
|
||||
onClick: () => show({
|
||||
title: t('UserPreferencesModal:User Preferences'),
|
||||
content: UserPreferences,
|
||||
contentProps: {
|
||||
hotkeyDefaults: hotkeysManager.getValidHotkeyDefinitions(hotkeyDefaults),
|
||||
hotkeyDefinitions,
|
||||
onCancel: hide,
|
||||
onSubmit: ({ hotkeyDefinitions }) => {
|
||||
hotkeysManager.setHotkeys(hotkeyDefinitions);
|
||||
hide();
|
||||
},
|
||||
onReset: () => hotkeysManager.restoreDefaultBindings()
|
||||
}
|
||||
})
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classnames('bg-black h-full', {
|
||||
'h-screen': !hasStudies,
|
||||
})}
|
||||
>
|
||||
<NavBar className="justify-between border-b-4 border-black" isSticky>
|
||||
<div className="flex items-center">
|
||||
<div className="mx-3">
|
||||
<Svg name="logo-ohif" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<span className="mr-3 text-lg text-common-light">
|
||||
FOR INVESTIGATIONAL USE ONLY
|
||||
</span>
|
||||
<PreferencesDropdown />
|
||||
</div>
|
||||
</NavBar>
|
||||
<Header menuOptions={menuOptions} isReturnEnabled={false} />
|
||||
<StudyListFilter
|
||||
numOfStudies={numOfStudies}
|
||||
filtersMeta={filtersMeta}
|
||||
|
||||
@ -51,7 +51,13 @@ const createRoutes = ({
|
||||
render={props => (
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
<ErrorBoundary context={`Route ${route.path}`} fallbackRoute="/">
|
||||
<route.component {...props} {...route.props} route={route} />
|
||||
<route.component
|
||||
{...props}
|
||||
{...route.props}
|
||||
route={route}
|
||||
servicesManager={servicesManager}
|
||||
hotkeysManager={hotkeysManager}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
)}
|
||||
/>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user