Compare commits

...
Author SHA1 Message Date
ohif-bot 9c1112f768 chore(release): publish [skip ci]
- @ohif/extension-vtk@1.2.21
 - @ohif/ui@1.3.1
 - @ohif/viewer@3.7.2
2020-02-29 14:01:25 +00:00
arthur-miller 9b3be9b0c0 fix: prevent the native context menu from appearing when right-clicking on a measurement or angle (https://github.com/OHIF/Viewers/issues/1406) (#1469) 2020-02-29 08:59:47 -05:00
ohif-bot 06075ecb51 chore(release): publish [skip ci]
- @ohif/extension-vtk@1.2.20
 - @ohif/core@2.5.1
 - @ohif/viewer@3.7.1
2020-02-21 02:44:01 +00:00
Igor Octaviano 509a7b7e57 fix: 💡 Update log to warn (#1454)
Update measurement service log to warn

✅ Closes: #1451
2020-02-20 21:41:01 -05:00
ohif-bot 4bbf24b250 chore(release): publish [skip ci]
- @ohif/extension-cornerstone@2.4.0
 - @ohif/extension-vtk@1.2.19
 - @ohif/core@2.5.0
 - @ohif/ui@1.3.0
 - @ohif/viewer@3.7.0
2020-02-20 22:04:03 +00:00
Gustavo André Lelis ebc01a8ca2 feat: #1342 - Window level tab (#1429)
* WindowLevel preset code changes

* Remove localStorage code as its already being saved and some small refactor on reducer/action

* Creating unit tests for preferences reducer

* Fix cypress after class naming change

* Make hidden false as default for tab components

* Remove addUserPreferences

* Small refactor to use commandsManager from getCommandsModule instead of getting it from App.js
2020-02-20 16:25:26 -05:00
ohif-bot 4fd0c6dfcb chore(release): publish [skip ci]
- @ohif/extension-cornerstone@2.3.1
 - @ohif/viewer@3.6.3
2020-02-14 20:56:06 +00:00
Gustavo André Lelis 06a4af06fa fix: Creating 2 commands to activate zoom tool and also to move between displaySets (#1446) 2020-02-14 15:54:08 -05:00
ohif-bot 6cba41dd8b chore(release): publish [skip ci]
- @ohif/extension-vtk@1.2.18
 - @ohif/core@2.4.1
 - @ohif/i18n@0.52.6
 - @ohif/ui@1.2.1
 - @ohif/viewer@3.6.2
2020-02-12 20:36:52 +00:00
2f30e7a821 fix: Combined Hotkeys for special characters (#1233)
* fix: Combined Hotkeys for special characters

* add record method to hotkey manager

* fix record plugin

* remove unused component

* add record to modal props

* rename record method

* replace handlers to use hotkeyRecord

* fix combined keys

* change expected result count from 18 to 17

* autoformat

* Remove duplicate test, that was testing the wrong things; fix label; update configs

* Revert "Remove duplicate test, that was testing the wrong things; fix label; update configs"

This reverts commit 4292f4fe67351962d61cae623b920dcbd87dd71d.

* Fix the record plugin's registration

* fix exposed record method usage

* adding logging for info level items

* Hotkey definitions don't need to be globally reactive; use localstorage/appconfig as sources of truth; not redux

* Tidy up test

* Remove unused code from UserPreferencesForm

* Log info when we run a command

* fix hotkey preference restore

* use application configured hotkeys if there are no user preferred

* Avoid logging circular ref

* Fix callouts

* Fix small issue with array

* Fix langua issue after refactor and merge

* Refactor on recordCurrentCombo as Rodrigo did before

* Separating components in 2 files

* WIP Refactor to simplify the user preferences and move into each form the save and controll functionalities

* Remove context

* Remove unused import

* Initial work on Field treatment

* Refactor General preferences

* Small refactor removing type from HotkeyField

* small update on style

* Refactor and layout fixed

* Make hotkeys preferences working with old hotkeys row

* Move error handling out of hotkey row/input component

* WIP custom form

* Moving validation function to component

* Exposing hotkeyRecord as it does not depend on HotkeyManager Class

* Making hotkeyField as much detached possible from parent component

* Small refactors

* Refactor on user preferences

* Clean up into the changes

* Small fix to let save working

* Style finish

* move about docs into about folder

* Fix double tap on single keys

* Style refactor

* Remove log

* Fix log issues on unit tests

* Fix unit test breaking on ohif/core index

* Fixing hotkeys unpause unit test issue

* Rename file to adopt lowercase

* Rename file to adopt lowercase

* Fixing callouts

* Big refactor miving some of the components into viewer and creating small components into ohif/ui

* Typo on folder name

* Updating ohif ui docs

* Remove comments

* Fix binding of combo keys

* Fix some cypress tests failures

* Fixing onCancel button

* Fixing e2e tests

* Small style update

* Fixing unit tests failing after fix issue

* Remove some not used code

* Remove left over after debug

* Adding prevent default on hotkeys events

* Fixinf existing hotkeys validator with 3 keys pressed

* Exposing hotkeys as root level on ohif-core

* Clean up

* Exposing all availableLanguages with labels and fixing an issue on language switcher

* Fixing e2e cypress tests

* Preveinting some simple errors

* Treating error once we try to set hotkey definitions

* Adding ui notification on setHotkeys errors

* Implementing a service queue request to hold until functions are implemented

* Making sure toFixed is only called on Numbers

Co-authored-by: Danny Brown <danny.ri.brown@gmail.com>
Co-authored-by: Gustavo André Lelis <galelis@gmail.com>
2020-02-12 15:35:04 -05:00
ohif-bot 9a62c28b3f chore(release): publish [skip ci]
- @ohif/extension-lesion-tracker@0.2.0
 - @ohif/extension-vtk@1.2.17
 - @ohif/ui@1.2.0
 - @ohif/viewer@3.6.1
2020-02-10 20:36:28 +00:00
Igor Octaviano 98a649b455 feat: Lesion tracker right panel (#1428)
* WIP

* Finish mock

* Update name, remove vendor prefix

* Remove lesion tracker from defaults

* Refactor measurement table changes

* Remove unused code

* Update naming of collections
2020-02-10 15:33:06 -05:00
102 changed files with 2198 additions and 1808 deletions

No files matched your search

+22
View File
@@ -3,6 +3,28 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [2.4.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.3.1...@ohif/extension-cornerstone@2.4.0) (2020-02-20)
### Features
* [#1342](https://github.com/OHIF/Viewers/issues/1342) - Window level tab ([#1429](https://github.com/OHIF/Viewers/issues/1429)) ([ebc01a8](https://github.com/OHIF/Viewers/commit/ebc01a8ca238d5a3437b44d81f75aa8a5e8d0574))
## [2.3.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.3.0...@ohif/extension-cornerstone@2.3.1) (2020-02-14)
### Bug Fixes
* Creating 2 commands to activate zoom tool and also to move between displaySets ([#1446](https://github.com/OHIF/Viewers/issues/1446)) ([06a4af0](https://github.com/OHIF/Viewers/commit/06a4af06faaecf6fa06ccd90cdfa879ee8d53053))
# [2.3.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.2.2...@ohif/extension-cornerstone@2.3.0) (2020-02-10)
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/extension-cornerstone",
"version": "2.3.0",
"version": "2.4.0",
"description": "OHIF extension for Cornerstone",
"author": "OHIF",
"license": "MIT",
+23 -17
View File
@@ -74,10 +74,6 @@ const commandsModule = ({ servicesManager }) => {
}
cornerstoneTools.setToolActive(toolName, { mouseButtonMask: 1 });
},
updateViewportDisplaySet: ({ direction }) => {
// TODO
console.warn('updateDisplaySet: ', direction);
},
clearAnnotations: ({ viewports }) => {
const element = getEnabledElement(viewports.activeViewportIndex);
if (!element) {
@@ -245,7 +241,19 @@ const commandsModule = ({ servicesManager }) => {
},
setCornerstoneLayout: () => {
setCornerstoneLayout();
}
},
setWindowLevel: ({ viewports, window, level }) => {
const enabledElement = getEnabledElement(viewports.activeViewportIndex);
if (enabledElement) {
let viewport = cornerstone.getViewport(enabledElement);
viewport.voi = {
windowWidth: Number(window),
windowCenter: Number(level),
};
cornerstone.setViewport(enabledElement, viewport);
}
},
};
const definitions = {
@@ -334,30 +342,28 @@ const commandsModule = ({ servicesManager }) => {
storeContexts: ['viewports'],
options: {},
},
// TODO: First/Last image
// Next/Previous series/DisplaySet
nextViewportDisplaySet: {
commandFn: actions.updateViewportDisplaySet,
storeContexts: [],
options: { direction: 1 },
},
previousViewportDisplaySet: {
commandFn: actions.updateViewportDisplaySet,
storeContexts: [],
options: { direction: -1 },
},
// TOOLS
setToolActive: {
commandFn: actions.setToolActive,
storeContexts: [],
options: {},
},
setZoomTool: {
commandFn: actions.setToolActive,
storeContexts: [],
options: { toolName: 'Zoom' },
},
setCornerstoneLayout: {
commandFn: actions.setCornerstoneLayout,
storeContexts: [],
options: {},
context: 'VIEWER',
},
setWindowLevel: {
commandFn: actions.setWindowLevel,
storeContexts: ['viewports'],
options: {},
},
};
return {
+11
View File
@@ -3,6 +3,17 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [0.2.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-lesion-tracker@0.1.0...@ohif/extension-lesion-tracker@0.2.0) (2020-02-10)
### Features
* Lesion tracker right panel ([#1428](https://github.com/OHIF/Viewers/issues/1428)) ([98a649b](https://github.com/OHIF/Viewers/commit/98a649b455ffc712938fc5035cdef40695e58440))
# 0.1.0 (2020-02-06)
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/extension-lesion-tracker",
"version": "0.1.0",
"version": "0.2.0",
"description": "OHIF extension for Lesion Tracker",
"author": "OHIF",
"license": "MIT",
@@ -0,0 +1,60 @@
.MeasurementComparisonTable {
height: 100%;
display: flex;
flex-direction: column;
}
.MeasurementComparisonTable .displayTexts {
display: flex;
}
.MeasurementComparisonTable .displayTexts .measurementDisplayText {
flex: 1;
}
.MeasurementComparisonTable .measurementTableHeader {
display: flex;
position: relative;
padding-left: 38px;
}
.MeasurementComparisonTable .measurementTableHeader .warning-status {
left: 0;
top: 3px;
cursor: pointer;
width: 40px;
height: 100%;
display: flex;
align-items: center;
}
.MeasurementComparisonTable .measurementTableHeader .warning-status .warning-border {
padding: 2px 3px;
width: 30px;
height: 30px;
}
.MeasurementComparisonTable .measurementTableHeader .warning-status svg {
width: 20px;
height: 18px;
}
.MeasurementComparisonTable .measurementTableHeader .measurementTableHeaderItem {
position: relative;
flex: 1;
}
.MeasurementComparisonTable .generate-report {
background-color: #151a1f;
margin-top: 2px;
padding: 10px 0;
display: flex;
justify-content: center;
align-items: center;
}
.MeasurementComparisonTable .generate-report button {
font-size: 12px;
color: black;
background-color: var(--active-color);
}
@@ -0,0 +1,151 @@
import React from 'react';
import { MeasurementTable } from '@ohif/ui';
import './MeasurementComparisonTable.css';
const overallWarnings = {
warningList: [
'All measurements should have a location',
'Nodal lesions must be >= 15mm short axis AND >= double the acquisition slice thickness by CT and MR',
],
};
const measurements = [
{
measurementId: '125',
measurementNumber: '125',
itemNumber: 1,
label: '(No description)',
data: [{ displayText: '12.5 x 4.6' }],
},
{
measurementId: '124',
measurementNumber: '124',
itemNumber: 2,
label: '(No description)',
data: [{ displayText: '32.5 x 1.6' }],
},
{
measurementId: '123',
measurementNumber: '123',
itemNumber: 3,
hasWarnings: true,
warningList: [
'All measurements should have a location',
'Nodal lesions must be >= 15mm short axis AND >= double the acquisition slice thickness by CT and MR',
],
label: '(No description)',
data: [{ displayText: '5.5 x 9.2' }],
},
];
const additionalFindings = [
{
measurementId: '122',
measurementNumber: '122',
itemNumber: 1,
hasWarnings: true,
warningList: [
'All measurements should have a location',
'Nodal lesions must be >= 15mm short axis AND >= double the acquisition slice thickness by CT and MR',
],
label: '(No description)',
data: [{ displayText: '23.5 x 9.2' }],
},
{
measurementId: '121',
measurementNumber: '121',
itemNumber: 2,
hasWarnings: true,
warningList: [
'All measurements should have a location',
'Nodal lesions must be >= 15mm short axis AND >= double the acquisition slice thickness by CT and MR',
],
label: '(No description)',
data: [{ displayText: '11.2 x 9.2' }],
},
{
measurementId: '120',
measurementNumber: '120',
itemNumber: 3,
label: '(No description)',
data: [{ displayText: '2.9 x 9.2' }],
},
];
const currentCollections = [
{
selectorAction: () => { },
maxMeasurements: 3,
groupName: 'Measurements',
measurements: measurements,
},
{
selectorAction: () => { },
groupName: 'Additional Findings',
measurements: additionalFindings,
},
];
const comparisonColletions = [
{
selectorAction: () => { },
maxMeasurements: 3,
groupName: 'Measurements',
measurements: measurements,
},
{
selectorAction: () => { },
groupName: 'Additional Findings',
measurements: additionalFindings,
},
];
const comparisonCollections = currentCollections.map((group, index) => {
return {
...group,
measurements: group.measurements.map((measurement, measurementIndex) => {
const comparisonCollection = comparisonColletions[index].measurements;
if (measurementIndex < comparisonCollection.length) {
return {
...measurement,
data: [
...measurement.data,
...comparisonCollection[measurementIndex].data,
],
};
}
}),
};
});
const comparisonTimepoints = [
{
key: 'Current',
date: '10-Apr-18',
},
{
key: 'Comparison',
date: '15-Jun-18',
}
];
const MeasurementComparisonTable = () => {
return (
<div className="MeasurementComparisonTable">
<MeasurementTable
timepoints={comparisonTimepoints}
overallWarnings={overallWarnings}
measurementCollection={comparisonCollections}
onRelabelClick={() => { }}
onEditDescriptionClick={() => { }}
/>
<div className="generate-report">
<button className="btn btn-primary">Generate Report</button>
</div>
</div>
);
};
export default MeasurementComparisonTable;
+24 -73
View File
@@ -1,12 +1,4 @@
// SEE: Async imports (code-splitting)
// import asyncComponent from './asyncComponent.js';
// import OHIFDicomPDFSopClassHandler from './OHIFDicomPDFSopClassHandler.js';
// const ConnectedOHIFDicomPDFViewer = asyncComponent(() =>
// import(
// /* webpackChunkName: "ConnectedOHIFDicomPDFViewer" */ './ConnectedOHIFDicomPDFViewer'
// )
// );
import MeasurementComparisonTable from './components/MeasurementComparisonTable';
export default {
/**
@@ -20,56 +12,21 @@ export default {
* @param {CommandsManager} params.commandsManager
*/
getPanelModule({ servicesManager, commandsManager }) {
return undefined;
// {
// TODO:
// - Right, alternative StudyBrowser
// - Left, alternative Measurements Table
// menuOptions: [
// {
// // A suggested icon
// // Available icons determined by consuming app
// icon: 'list',
// // A suggested label
// label: 'Magic',
// // 'right' or 'left'
// from: 'right',
// // The target component to toggle open/close
// target: 'target-component-id',
// // Overrides `defaultContext`, if specified
// context: ['ACTIVE_VIEWPORT:MAGIC'],
// },
// ],
// components: [
// {
// id: 'target-component-id',
// component: MyComponent,
// },
// ],
// defaultContext: ['ROUTE:VIEWER'],
// };
},
/**
* @param {object} params
* @param {ServicesManager} params.servicesManager
* @param {CommandsManager} params.commandsManager
* @returns Object
*/
getToolbarModule({ servicesManager, commandsManager }) {
// TODO: Zoom, Levels, Pan, More (see lesiontracker.ohif.org)
// These may be driven through configuration of `cornerstone` extension instead of defined here
return {
definitions: [
menuOptions: [
{
id: 'say-hell-world',
label: '🎉 HELLO WORLD 🎉',
icon: 'exclamation-triangle',
type: 'command',
commandName: 'sayHelloWorld',
icon: 'th-list',
label: 'Measurements',
target: 'lesion-tracker-panel',
},
],
defaultContext: 'VIEWER',
components: [
{
id: 'lesion-tracker-panel',
component: MeasurementComparisonTable,
},
],
defaultContext: ['VIEWER']
};
},
@@ -79,23 +36,17 @@ export default {
* @param {CommandsManager} params.commandsManager
* @returns Object
*/
getCommandsModule({ servicesManager }) {
const { UINotificationService } = servicesManager.services;
return {
definitions: {
sayHelloWorld: {
commandFn: function() {
console.log(UINotificationService);
UINotificationService.show({
title: 'What does a nosey pepper do?',
message: 'Gets jalapeno business!',
});
},
storeContexts: [],
options: {},
},
},
defaultContext: ['VIEWER'],
};
getToolbarModule() {
return null;
},
/**
* @param {object} params
* @param {ServicesManager} params.servicesManager
* @param {CommandsManager} params.commandsManager
* @returns Object
*/
getCommandsModule() {
return null;
},
};
+40
View File
@@ -3,6 +3,46 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.2.21](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.2.20...@ohif/extension-vtk@1.2.21) (2020-02-29)
**Note:** Version bump only for package @ohif/extension-vtk
## [1.2.20](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.2.19...@ohif/extension-vtk@1.2.20) (2020-02-21)
**Note:** Version bump only for package @ohif/extension-vtk
## [1.2.19](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.2.18...@ohif/extension-vtk@1.2.19) (2020-02-20)
**Note:** Version bump only for package @ohif/extension-vtk
## [1.2.18](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.2.17...@ohif/extension-vtk@1.2.18) (2020-02-12)
**Note:** Version bump only for package @ohif/extension-vtk
## [1.2.17](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.2.16...@ohif/extension-vtk@1.2.17) (2020-02-10)
**Note:** Version bump only for package @ohif/extension-vtk
## [1.2.16](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.2.15...@ohif/extension-vtk@1.2.16) (2020-02-10)
**Note:** Version bump only for package @ohif/extension-vtk
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/extension-vtk",
"version": "1.2.16",
"version": "1.2.21",
"description": "OHIF extension for VTK.js",
"author": "OHIF",
"license": "MIT",
@@ -52,8 +52,8 @@
"react-vtkjs-viewport": "^0.8.3"
},
"devDependencies": {
"@ohif/core": "^2.4.0",
"@ohif/ui": "^1.1.9",
"@ohif/core": "^2.5.1",
"@ohif/ui": "^1.3.1",
"cornerstone-tools": "^4.12.0",
"cornerstone-wado-image-loader": "^3.0.0",
"dicom-parser": "^1.8.3",
+33
View File
@@ -3,6 +3,39 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [2.5.1](https://github.com/OHIF/Viewers/compare/@ohif/core@2.5.0...@ohif/core@2.5.1) (2020-02-21)
### Bug Fixes
* 💡 Update log to warn ([#1454](https://github.com/OHIF/Viewers/issues/1454)) ([509a7b7](https://github.com/OHIF/Viewers/commit/509a7b7e57834a0653add98cc320b2a5dbefd51d)), closes [#1451](https://github.com/OHIF/Viewers/issues/1451)
# [2.5.0](https://github.com/OHIF/Viewers/compare/@ohif/core@2.4.1...@ohif/core@2.5.0) (2020-02-20)
### Features
* [#1342](https://github.com/OHIF/Viewers/issues/1342) - Window level tab ([#1429](https://github.com/OHIF/Viewers/issues/1429)) ([ebc01a8](https://github.com/OHIF/Viewers/commit/ebc01a8ca238d5a3437b44d81f75aa8a5e8d0574))
## [2.4.1](https://github.com/OHIF/Viewers/compare/@ohif/core@2.4.0...@ohif/core@2.4.1) (2020-02-12)
### Bug Fixes
* Combined Hotkeys for special characters ([#1233](https://github.com/OHIF/Viewers/issues/1233)) ([2f30e7a](https://github.com/OHIF/Viewers/commit/2f30e7a821a238144c49c56f37d8e5565540b4bd))
# [2.4.0](https://github.com/OHIF/Viewers/compare/@ohif/core@2.3.9...@ohif/core@2.4.0) (2020-02-10)
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/core",
"version": "2.4.0",
"version": "2.5.1",
"description": "Generic business logic for web-based medical imaging applications",
"author": "OHIF Core Team",
"license": "MIT",
+1
View File
@@ -1,4 +1,5 @@
export default {
warn: jest.fn(),
error: jest.fn(),
info: jest.fn(),
};
+50 -19
View File
@@ -1,4 +1,4 @@
import hotkeys from './hotkeys';
import hotkeys from './../utils/hotkeys';
import log from './../log.js';
/**
@@ -11,7 +11,7 @@ import log from './../log.js';
*/
export class HotkeysManager {
constructor(commandsManager) {
constructor(commandsManager, servicesManager) {
this.hotkeyDefinitions = {};
this.hotkeyDefaults = [];
this.isEnabled = true;
@@ -22,9 +22,19 @@ export class HotkeysManager {
);
}
this._servicesManager = servicesManager;
this._commandsManager = commandsManager;
}
/**
* Exposes Mousetrap.js's `.record` method, added by the record plugin.
*
* @param {*} event
*/
record(event) {
return hotkeys.record(event);
}
/**
* Disables all hotkeys. Hotkeys added while disabled will not listen for
* input.
@@ -45,38 +55,57 @@ export class HotkeysManager {
/**
* Registers a list of hotkeydefinitions.
*
* @param {HotkeyDefinition[] | Object} hotkeyDefinitions Contains hotkeys definitions
* @param {HotkeyDefinition[] | Object} [hotkeyDefinitions=[]] Contains hotkeys definitions
*/
setHotkeys(hotkeyDefinitions) {
const definitions = Array.isArray(hotkeyDefinitions)
? [...hotkeyDefinitions]
: this._parseToArrayLike(hotkeyDefinitions);
setHotkeys(hotkeyDefinitions = []) {
try {
const definitions = this._getValidDefinitions(hotkeyDefinitions);
definitions.forEach(definition => this.registerHotkeys(definition));
definitions.forEach(definition => this.registerHotkeys(definition));
} catch (error) {
const { UINotificationService } = this._servicesManager.services;
UINotificationService.show({
title: 'Hotkeys Manager',
message: 'Erro while setting hotkeys',
type: 'error',
});
}
}
/**
* Set default hotkey bindings. These
* values are used in `this.restoreDefaultBindings`.
*
* @param {HotkeyDefinition[] | Object} hotkeyDefinitions Contains hotkeys definitions
* @param {HotkeyDefinition[] | Object} [hotkeyDefinitions=[]] Contains hotkeys definitions
*/
setDefaultHotKeys(hotkeyDefinitions) {
setDefaultHotKeys(hotkeyDefinitions = []) {
const definitions = this._getValidDefinitions(hotkeyDefinitions);
this.hotkeyDefaults = definitions;
}
/**
* Take hotkey definitions that can be an array or object and make sure that it
* returns an array of hotkeys
*
* @param {HotkeyDefinition[] | Object} [hotkeyDefinitions=[]] Contains hotkeys definitions
*/
_getValidDefinitions(hotkeyDefinitions) {
const definitions = Array.isArray(hotkeyDefinitions)
? [...hotkeyDefinitions]
: this._parseToArrayLike(hotkeyDefinitions);
this.hotkeyDefaults = definitions;
return definitions;
}
/**
* It parses given object containing hotkeyDefinition to array like.
* Each property of given object will be mapped to an object of an array. And its property name will be the value of a property named as commandName
*
* @param {HotkeyDefinition[] | Object} hotkeyDefinitions Contains hotkeys definitions
* @param {HotkeyDefinition[] | Object} [hotkeyDefinitions={}] Contains hotkeys definitions
* @returns {HotkeyDefinition[]}
*/
_parseToArrayLike(hotkeyDefinitionsObj) {
_parseToArrayLike(hotkeyDefinitionsObj = {}) {
const copy = { ...hotkeyDefinitionsObj };
return Object.entries(copy).map(entryValue =>
this._parseToHotKeyObj(entryValue[0], entryValue[1])
@@ -127,11 +156,13 @@ export class HotkeysManager {
if (previouslyRegisteredDefinition) {
const previouslyRegisteredKeys = previouslyRegisteredDefinition.keys;
this._unbindHotkeys(commandName, previouslyRegisteredKeys);
log.info(`Unbinding ${commandName} from ${previouslyRegisteredKeys}`);
}
// Set definition & bind
this.hotkeyDefinitions[commandName] = { keys, label };
this._bindHotkeys(commandName, keys);
log.info(`Binding ${commandName} to ${keys}`);
}
/**
@@ -167,12 +198,11 @@ export class HotkeysManager {
}
const isKeyArray = keys instanceof Array;
if (isKeyArray) {
keys.forEach(key => this._bindHotkeys(commandName, key));
return;
}
const combinedKeys = isKeyArray ? keys.join('+') : keys;
hotkeys.bind(keys, evt => {
hotkeys.bind(combinedKeys, evt => {
evt.preventDefault();
evt.stopPropagation();
this._commandsManager.runCommand(commandName, { evt });
});
}
@@ -193,7 +223,8 @@ export class HotkeysManager {
const isKeyArray = keys instanceof Array;
if (isKeyArray) {
keys.forEach(key => this._unbindHotkeys(commandName, key));
const combinedKeys = keys.join('+');
this._unbindHotkeys(commandName, combinedKeys);
return;
}
@@ -1,10 +1,10 @@
import CommandsManager from './CommandsManager.js';
import HotkeysManager from './HotkeysManager.js';
import hotkeys from './hotkeys';
import hotkeys from './../utils/hotkeys';
import log from './../log.js';
jest.mock('./CommandsManager.js');
jest.mock('./hotkeys');
jest.mock('./../utils/hotkeys');
jest.mock('./../log.js');
describe('HotkeysManager', () => {
@@ -60,7 +60,10 @@ describe('HotkeysManager', () => {
});
describe('enable()', () => {
beforeEach(() => hotkeys.unpause.mockClear());
beforeEach(() => {
hotkeys.unpause = jest.fn();
hotkeys.unpause.mockClear();
});
it('sets isEnabled property to true', () => {
hotkeysManager.disable();
@@ -139,20 +142,18 @@ describe('HotkeysManager', () => {
expectedHotkeyDefinition
);
});
it('calls hotkeys.bind for all keys in array', () => {
const definition = { commandName: 'dance', keys: ['h', 'e', 'l', 'o'] };
it('calls hotkeys.bind for the group of keys', () => {
const definition = { commandName: 'dance', keys: ['shift', 'e'] };
hotkeysManager.registerHotkeys(definition);
expect(hotkeys.bind.mock.calls.length).toBe(definition.keys.length);
definition.keys.forEach((key, i) =>
expect(hotkeys.bind.mock.calls[i][0]).toBe(key)
);
expect(hotkeys.bind.mock.calls.length).toBe(1);
expect(hotkeys.bind.mock.calls[0][0]).toBe('shift+e');
});
it('calls hotkeys.unbind if commandName was previously registered, for each previously registered set of keys', () => {
const firstDefinition = {
commandName: 'dance',
keys: ['h', 'e', 'l', 'o'],
keys: ['alt', 'e'],
};
const secondDefinition = { commandName: 'dance', keys: 'a' };
@@ -161,12 +162,8 @@ describe('HotkeysManager', () => {
// Second call
hotkeysManager.registerHotkeys(secondDefinition);
expect(hotkeys.unbind.mock.calls.length).toBe(
firstDefinition.keys.length
);
firstDefinition.keys.forEach((key, i) =>
expect(hotkeys.unbind.mock.calls[i][0]).toBe(key)
);
expect(hotkeys.unbind.mock.calls.length).toBe(1);
expect(hotkeys.unbind.mock.calls[0][0]).toBe('alt+e');
});
});
@@ -1,17 +0,0 @@
// Only imported in environment w/ `window`
// So we need to mock these for tests
import Mousetrap from 'mousetrap';
import pausePlugin from 'mousetrap/plugins/pause/mousetrap-pause.js';
import recordPlugin from 'mousetrap/plugins/record/mousetrap-record.js';
// import pausePlugin from './pausePlugin.js';
// import recordPlugin from './recordPlugin.js';
// // // TODO: May need to bind these so Mousetrap = this in plugins;
// pausePlugin(Mousetrap);
// recordPlugin(Mousetrap);
// console.log(Mousetrap);
// console.log(Object.keys(Mousetrap));
export default Mousetrap;
+3 -1
View File
@@ -18,7 +18,7 @@ import string from './string.js';
import studies from './studies/';
import ui from './ui';
import user from './user.js';
import utils from './utils/';
import utils, { hotkeys } from './utils/';
import {
UINotificationService,
@@ -36,6 +36,7 @@ const OHIF = {
ServicesManager,
//
utils,
hotkeys,
studies,
redux,
classes,
@@ -68,6 +69,7 @@ export {
ServicesManager,
//
utils,
hotkeys,
studies,
redux,
classes,
+1
View File
@@ -16,6 +16,7 @@ describe('Top level exports', () => {
'MeasurementService',
//
'utils',
'hotkeys',
'studies',
'redux',
'classes',
@@ -1,6 +1,6 @@
const displayFunction = data => {
let text = '';
if (data.rAngle) {
if (data.rAngle && !isNaN(data.rAngle)) {
text = data.rAngle.toFixed(2) + String.fromCharCode(parseInt('00B0', 16));
}
return text;
@@ -1,7 +1,7 @@
const displayFunction = data => {
let meanValue = '';
const { cachedStats } = data;
if (cachedStats && cachedStats.mean) {
if (cachedStats && cachedStats.mean && !isNaN(cachedStats.mean)) {
meanValue = cachedStats.mean.toFixed(2) + ' HU';
}
return meanValue;
@@ -1,7 +1,7 @@
const displayFunction = data => {
let meanValue = '';
const { cachedStats } = data;
if (cachedStats && cachedStats.mean) {
if (cachedStats && cachedStats.mean && !isNaN(cachedStats.mean)) {
meanValue = cachedStats.mean.toFixed(2) + ' HU';
}
return meanValue;
@@ -1,6 +1,6 @@
const displayFunction = data => {
let meanValue = '';
if (data.meanStdDev && data.meanStdDev.mean) {
if (data.meanStdDev && data.meanStdDev.mean && !isNaN(data.meanStdDev.mean)) {
meanValue = data.meanStdDev.mean.toFixed(2) + ' HU';
}
return meanValue;
@@ -1,6 +1,6 @@
const displayFunction = data => {
let lengthValue = '';
if (data.length) {
if (data.length && !isNaN(data.length)) {
lengthValue = data.length.toFixed(2) + ' mm';
}
return lengthValue;
@@ -1,7 +1,7 @@
const displayFunction = data => {
let meanValue = '';
const { cachedStats } = data;
if (cachedStats && cachedStats.mean) {
if (cachedStats && cachedStats.mean && !isNaN(cachedStats.mean)) {
meanValue = cachedStats.mean.toFixed(2) + ' HU';
}
return meanValue;
+2 -1
View File
@@ -10,6 +10,7 @@ import {
SET_VIEWPORT_ACTIVE,
SET_VIEWPORT_LAYOUT,
SET_VIEWPORT_LAYOUT_AND_DATA,
SET_USER_PREFERENCES,
} from './constants/ActionTypes.js';
/**
@@ -89,7 +90,7 @@ export const clearStudyLoadingProgress = progressId => ({
});
export const setUserPreferences = state => ({
type: 'SET_USER_PREFERENCES',
type: SET_USER_PREFERENCES,
state,
});
@@ -20,3 +20,8 @@ export const SET_SERVERS = 'SET_SERVERS';
* EXTENSIONS
*/
export const SET_EXTENSION_DATA = 'SET_EXTENSION_DATA';
/**
* PREFERENCES
* */
export const SET_USER_PREFERENCES = 'SET_USER_PREFERENCES';
+14 -15
View File
@@ -1,31 +1,30 @@
import cloneDeep from 'lodash.clonedeep';
const defaultState = {
// First tab
hotkeyDefinitions: [
// commandName, label, keys
// [{ zoom: { label: 'Zoom', keys: ['z'] }}]
],
// Second tab
windowLevelData: {
// order, description, window (int), level (int)
// 0: { description: 'Soft tissue', window: '', level: '' },
1: { description: 'Soft tissue', window: '550', level: '40' },
2: { description: 'Lung', window: '150', level: '-600' },
3: { description: 'Liver', window: '150', level: '90' },
4: { description: 'Bone', window: '2500', level: '480' },
5: { description: 'Brain', window: '80', level: '40' },
6: { description: 'Trest', window: '1', level: '1' },
7: { description: '', window: '', level: '' },
8: { description: '', window: '', level: '' },
9: { description: '', window: '', level: '' },
10: { description: '', window: '', level: '' },
},
generalPreferences: {
// language: 'en-US'
},
};
const preferences = (state, action) => {
const preferences = (state = defaultState, action) => {
switch (action.type) {
case 'SET_USER_PREFERENCES': {
const newState = action.state || cloneDeep(defaultState);
return Object.assign({}, state, newState);
return Object.assign({}, state, action.state);
}
default:
return cloneDeep(state) || cloneDeep(defaultState);
return state;
}
};
export { defaultState };
export default preferences;
@@ -0,0 +1,31 @@
import { Reducer } from 'redux-testkit';
import reducer, { defaultState } from './preferences';
import { SET_USER_PREFERENCES } from './../constants/ActionTypes.js';
describe('preferences reducer', () => {
it('should return the initial state', () => {
expect(reducer(undefined, {})).toEqual(defaultState);
});
it('should set user preferences state and properly merge with current state', () => {
const initialState = defaultState;
const action = {
type: SET_USER_PREFERENCES,
state: { generalPreferences: { language: 'es' } },
};
const expectedState = {
windowLevelData: defaultState.windowLevelData,
generalPreferences: {
language: 'es',
},
};
Reducer(reducer)
.withState(initialState)
.expect(action)
.toReturnState(expectedState);
});
});
@@ -271,7 +271,7 @@ class MeasurementService {
/* Assign measurement source instance */
measurement.source = source;
} catch (error) {
log.error(`Failed to map '${sourceInfo}' measurement for definition ${definition}:`, error.message);
log.warn(`Failed to map '${sourceInfo}' measurement for definition ${definition}:`, error.message);
return;
}
@@ -262,7 +262,7 @@ describe('MeasurementService.js', () => {
source.addOrUpdate(definition, measurement);
expect(log.error.mock.calls.length).toBe(1);
expect(log.warn.mock.calls.length).toBe(1);
});
it('adds new measurement with custom id', () => {
@@ -12,6 +12,8 @@
const name = 'UINotificationService';
const serviceShowRequestQueue = [];
const publicAPI = {
name,
hide: _hide,
@@ -21,7 +23,11 @@ const publicAPI = {
const serviceImplementation = {
_hide: () => console.warn('hide() NOT IMPLEMENTED'),
_show: () => console.warn('show() NOT IMPLEMENTED'),
_show: showArguments => {
serviceShowRequestQueue.push(showArguments);
console.warn('show() NOT IMPLEMENTED');
},
};
/**
@@ -76,6 +82,11 @@ function setServiceImplementation({
}
if (showImplementation) {
serviceImplementation._show = showImplementation;
while (serviceShowRequestQueue.length > 0) {
const showArguments = serviceShowRequestQueue.pop();
serviceImplementation._show(showArguments);
}
}
}
+8
View File
@@ -0,0 +1,8 @@
import Mousetrap from 'mousetrap';
import pausePlugin from './pausePlugin';
import recordPlugin from './recordPlugin';
recordPlugin(Mousetrap);
pausePlugin(Mousetrap);
export default Mousetrap;
@@ -66,7 +66,7 @@ export default function(Mousetrap) {
_recordCurrentCombo();
}
for (i = 0; i < modifiers.length; ++i) {
for (let i = 0; i < modifiers.length; ++i) {
_recordKey(modifiers[i]);
}
_recordKey(character);
@@ -85,10 +85,8 @@ export default function(Mousetrap) {
* @returns void
*/
function _recordKey(key) {
var i;
// one-off implementation of Array.indexOf, since IE6-9 don't support it
for (i = 0; i < _currentRecordedKeys.length; ++i) {
for (let i = 0; i < _currentRecordedKeys.length; ++i) {
if (_currentRecordedKeys[i] === key) {
return;
}
@@ -111,7 +109,7 @@ export default function(Mousetrap) {
_recordedSequence.push(_currentRecordedKeys);
_currentRecordedKeys = [];
_recordedCharacterKey = false;
_restartRecordTimer();
_finishRecording();
}
/**
@@ -124,9 +122,7 @@ export default function(Mousetrap) {
* @returns void
*/
function _normalizeSequence(sequence) {
var i;
for (i = 0; i < sequence.length; ++i) {
for (let i = 0; i < sequence.length; ++i) {
sequence[i].sort(function(x, y) {
// modifier keys always come first, in alphabetical order
if (x.length > 1 && y.length === 1) {
@@ -191,6 +187,28 @@ export default function(Mousetrap) {
};
};
/**
* stop recording
*
* @param {Function} callback
* @returns void
*/
Mousetrap.prototype.stopRecord = function() {
var self = this;
self.recording = false;
};
/**
* start recording
*
* @param {Function} callback
* @returns void
*/
Mousetrap.prototype.startRecording = function() {
var self = this;
self.recording = true;
};
Mousetrap.prototype.handleKey = function() {
var self = this;
_handleKey.apply(self, arguments);
+3
View File
@@ -12,6 +12,7 @@ import DicomLoaderService from './dicomLoaderService.js';
import b64toBlob from './b64toBlob.js';
import * as urlUtil from './urlUtil';
import makeCancelable from './makeCancelable';
import hotkeys from './hotkeys';
const utils = {
guid,
@@ -29,6 +30,7 @@ const utils = {
DicomLoaderService,
urlUtil,
makeCancelable,
hotkeys,
};
export {
@@ -47,6 +49,7 @@ export {
DicomLoaderService,
urlUtil,
makeCancelable,
hotkeys,
};
export default utils;
+1
View File
@@ -18,6 +18,7 @@ describe('Top level exports', () => {
'DicomLoaderService',
'urlUtil',
'makeCancelable',
'hotkeys',
].sort();
const exports = Object.keys(utils.default).sort();
+11
View File
@@ -3,6 +3,17 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [0.52.6](https://github.com/OHIF/Viewers/compare/@ohif/i18n@0.52.5...@ohif/i18n@0.52.6) (2020-02-12)
### Bug Fixes
* Combined Hotkeys for special characters ([#1233](https://github.com/OHIF/Viewers/issues/1233)) ([2f30e7a](https://github.com/OHIF/Viewers/commit/2f30e7a821a238144c49c56f37d8e5565540b4bd))
## [0.52.5](https://github.com/OHIF/Viewers/compare/@ohif/i18n@0.52.4...@ohif/i18n@0.52.5) (2020-01-30)
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/i18n",
"version": "0.52.5",
"version": "0.52.6",
"description": "Internationalization library for The OHIF Viewer",
"author": "OHIF",
"license": "MIT",
@@ -0,0 +1,70 @@
const languagesMap = {
ar: 'Arabic',
am: 'Amharic',
bg: 'Bulgarian',
bn: 'Bengali',
ca: 'Catalan',
cs: 'Czech',
da: 'Danish',
de: 'German',
el: 'Greek',
en: 'English',
'en-GB': 'English (Great Britain)',
'en-US': 'English (USA)',
es: 'Spanish',
et: 'Estonian',
fa: 'Persian',
fi: 'Finnish',
fil: 'Filipino',
fr: 'French',
gu: 'Gujarati',
he: 'Hebrew',
hi: 'Hindi',
hr: 'Croatian',
hu: 'Hungarian',
id: 'Indonesian',
it: 'Italian',
ja: 'Japanese',
'ja-JP': 'Japanese (Japan)',
kn: 'Kannada',
ko: 'Korean',
lt: 'Lithuanian',
lv: 'Latvian',
ml: 'Malayalam',
mr: 'Marathi',
ms: 'Malay',
nl: 'Dutch',
no: 'Norwegian',
pl: 'Polish',
'pt-BR': 'Portuguese (Brazil)',
'pt-PT': 'Portuguese (Portugal)',
ro: 'Romanian',
ru: 'Russian',
sk: 'Slovak',
sl: 'Slovenian',
sr: 'Serbian',
sv: 'Swedish',
sw: 'Swahili',
ta: 'Tamil',
te: 'Telugu',
th: 'Thai',
tr: 'Turkish',
uk: 'Ukrainian',
vi: 'Vietnamese',
zh: 'Chinese',
'zh-CN': 'Chinese (China)',
'zh-TW': 'Chinese (Taiwan)',
};
export default function getAvailableLanguagesInfo(locales) {
const availableLanguagesInfo = [];
Object.keys(locales).forEach(key => {
availableLanguagesInfo.push({
value: key,
label: languagesMap[key] || key,
});
});
return availableLanguagesInfo;
}
+3
View File
@@ -140,4 +140,7 @@ i18n.initI18n = initI18n;
i18n.addLocales = addLocales;
i18n.defaultLanguage = DEFAULT_LANGUAGE;
import getAvailableLanguagesInfo from './getAvailableLanguagesInfo.js';
i18n.availableLanguages = getAvailableLanguagesInfo(locales);
export default i18n;
+44
View File
@@ -3,6 +3,50 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.3.1](https://github.com/OHIF/Viewers/compare/@ohif/ui@1.3.0...@ohif/ui@1.3.1) (2020-02-29)
### Bug Fixes
* prevent the native context menu from appearing when right-clicking on a measurement or angle (https://github.com/OHIF/Viewers/issues/1406) ([#1469](https://github.com/OHIF/Viewers/issues/1469)) ([9b3be9b](https://github.com/OHIF/Viewers/commit/9b3be9b0c082c9a5b62f2a40f42e59381860fe73))
# [1.3.0](https://github.com/OHIF/Viewers/compare/@ohif/ui@1.2.1...@ohif/ui@1.3.0) (2020-02-20)
### Features
* [#1342](https://github.com/OHIF/Viewers/issues/1342) - Window level tab ([#1429](https://github.com/OHIF/Viewers/issues/1429)) ([ebc01a8](https://github.com/OHIF/Viewers/commit/ebc01a8ca238d5a3437b44d81f75aa8a5e8d0574))
## [1.2.1](https://github.com/OHIF/Viewers/compare/@ohif/ui@1.2.0...@ohif/ui@1.2.1) (2020-02-12)
### Bug Fixes
* Combined Hotkeys for special characters ([#1233](https://github.com/OHIF/Viewers/issues/1233)) ([2f30e7a](https://github.com/OHIF/Viewers/commit/2f30e7a821a238144c49c56f37d8e5565540b4bd))
# [1.2.0](https://github.com/OHIF/Viewers/compare/@ohif/ui@1.1.9...@ohif/ui@1.2.0) (2020-02-10)
### Features
* Lesion tracker right panel ([#1428](https://github.com/OHIF/Viewers/issues/1428)) ([98a649b](https://github.com/OHIF/Viewers/commit/98a649b455ffc712938fc5035cdef40695e58440))
## [1.1.9](https://github.com/OHIF/Viewers/compare/@ohif/ui@1.1.8...@ohif/ui@1.1.9) (2020-01-30)
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/ui",
"version": "1.1.9",
"version": "1.3.1",
"description": "A set of React components for Medical Imaging Viewers",
"author": "OHIF Contributors",
"license": "MIT",
@@ -1,11 +1,10 @@
import React from 'react';
import PropTypes from 'prop-types';
import React from 'react';
import './ContextMenu.css';
const ContextMenu = ({ items, onClick }) => {
return (
<div className="ContextMenu">
<div className="ContextMenu" onContextMenu={e => e.preventDefault()}>
<ul>
{items.map((item, index) => (
<li key={index}>
@@ -0,0 +1,82 @@
import React from 'react';
import PropTypes from 'prop-types';
import { hotkeys } from '@ohif/core';
/**
* Take the pressed key array and return the readable string for the keys
*
* @param {Array} [keys=[]]
* @returns {string} string representation of an array of keys
*/
const formatKeysForInput = (keys = []) => keys.join('+');
/**
* formats given keys sequence to insert the modifier keys in the first index of the array
* @param {string} sequence keys sequence from MouseTrap Record -> "shift+left"
* @returns {Array} keys in array-format -> ['shift','left']
*/
const getKeys = ({ sequence, modifier_keys }) => {
const keysArray = sequence.join(' ').split('+');
let keys = [];
let modifiers = [];
keysArray.forEach(key => {
if (modifier_keys && modifier_keys.includes(key)) {
modifiers.push(key);
} else {
keys.push(key);
}
});
return [...modifiers, ...keys];
};
/**
* HotkeyField
* Renders a hotkey input
*
* @param {object} props component props
* @param {Array[]} props.keys array of keys to be controlled by this field
* @param {function} props.handleChange Callback function to communicate parent once value is changed
* @param {string} props.classNames string caontaining classes to be added in the input field
* @param {Array[]} props.modifier_keys
*/
function HotkeyField({ keys, handleChange, classNames, modifier_keys }) {
const inputValue = formatKeysForInput(keys);
const onInputKeyDown = event => {
event.stopPropagation();
event.preventDefault();
hotkeys.record(sequence => {
const keys = getKeys({ sequence, modifier_keys });
hotkeys.unpause();
handleChange(keys);
});
};
const onFocus = () => {
hotkeys.pause();
hotkeys.startRecording();
};
return (
<input
readOnly={true}
type="text"
value={inputValue}
className={classNames}
onKeyDown={onInputKeyDown}
onFocus={onFocus}
/>
);
}
HotkeyField.propTypes = {
keys: PropTypes.array.isRequired,
handleChange: PropTypes.func.isRequired,
classNames: PropTypes.string,
modifier_keys: PropTypes.array,
allowed_keys: PropTypes.array,
};
export { HotkeyField };
@@ -0,0 +1 @@
export { HotkeyField } from './HotkeyField';
+8 -7
View File
@@ -3,11 +3,10 @@ import { LayoutButton, LayoutChooser } from './layoutButton';
import { MeasurementTable, MeasurementTableItem } from './measurementTable';
import { Overlay, OverlayTrigger } from './overlayTrigger';
import { TableList, TableListItem } from './tableList';
import {
AboutContent,
UserPreferences,
UserPreferencesForm,
} from './userPreferencesForm';
import { AboutContent } from './content/aboutContent/AboutContent';
import { TabComponents, TabFooter } from './tabComponents';
import { HotkeyField } from './customForm';
import { LanguageSwitcher } from './languageSwitcher';
import { Checkbox } from './checkbox';
import { CineDialog } from './cineDialog';
@@ -48,12 +47,14 @@ export {
TableList,
TableListItem,
Thumbnail,
TabComponents,
TabFooter,
HotkeyField,
LanguageSwitcher,
TableSearchFilter,
TablePagination,
ToolbarSection,
Tooltip,
AboutContent,
UserPreferences,
UserPreferencesForm,
OHIFModal,
};
@@ -2,26 +2,11 @@ import React from 'react';
import PropTypes from 'prop-types';
import './LanguageSwitcher.styl';
import { withTranslation } from '../../contextProviders';
const LanguageSwitcher = ({ language, onLanguageChange }) => {
const parseLanguage = lang => lang.split('-')[0];
const languages = [
// TODO: list of available languages should come from i18n.options.resources
{
value: 'en',
label: 'English',
},
{
value: 'es',
label: 'Spanish',
},
];
const LanguageSwitcher = ({ language, onLanguageChange, languages }) => {
const onChange = event => {
const { value } = event.target;
onLanguageChange(parseLanguage(value));
onLanguageChange(value);
};
return (
@@ -29,7 +14,7 @@ const LanguageSwitcher = ({ language, onLanguageChange }) => {
name="language-select"
id="language-select"
className="language-select"
value={parseLanguage(language)}
value={language}
onChange={onChange}
>
{languages.map(lng => (
@@ -43,7 +28,8 @@ const LanguageSwitcher = ({ language, onLanguageChange }) => {
LanguageSwitcher.propTypes = {
language: PropTypes.string.isRequired,
languages: PropTypes.array.isRequired,
onLanguageChange: PropTypes.func.isRequired,
};
export default withTranslation('UserPreferencesModal')(LanguageSwitcher);
export { LanguageSwitcher };
@@ -1 +1 @@
export { default } from './LanguageSwitcher';
export { LanguageSwitcher } from './LanguageSwitcher';
@@ -184,10 +184,12 @@ class MeasurementTable extends Component {
};
getTimepointsHeader = () => {
return this.props.timepoints.map((timepoint, index) => {
const { timepoints, t } = this.props;
return timepoints.map((timepoint, index) => {
return (
<div key={index} className="measurementTableHeaderItem">
<div className="timepointLabel">{this.props.t(timepoint.key)}</div>
<div className="timepointLabel">{t(timepoint.key)}</div>
<div className="timepointDate">{timepoint.date}</div>
</div>
);
@@ -84,7 +84,6 @@
position: relative
top: 2px
.tooltip-warning
&.in
opacity 1
@@ -44,8 +44,8 @@ class MeasurementTableItem extends Component {
<div>{this.getTableListItem()}</div>
</OverlayTrigger>
) : (
<React.Fragment>{this.getTableListItem()}</React.Fragment>
)}
<React.Fragment>{this.getTableListItem()}</React.Fragment>
)}
</React.Fragment>
);
}
@@ -101,7 +101,7 @@ class MeasurementTableItem extends Component {
nsSeparator: '|',
})}
</div>
<div>{this.getDataDisplayText()}</div>
<div className="displayTexts">{this.getDataDisplayText()}</div>
<div className="rowActions">{actionButtons}</div>
</div>
</TableListItem>
@@ -0,0 +1,109 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
// Style
import './TabComponents.styl';
/**
* Take name of the tab and create the data-cy value for it
*
* @param {string} [name=''] tab name
* @returns {string} data-cy value
*/
const getDataCy = (name = '') => {
return name
.split(' ')
.join('-')
.toLowerCase();
};
/**
* Single tab data information
*
* @typedef {Object} tabData
* @property {string} name - name of the tab
* @property {Object} Component - tab component to be rendered
* @property {Object} customProps - tab custom properties
* @property {bool} hidden - bool to define if tab is hidden of not
*/
/**
* Take a list of components data and render then into tabs
*
* @param {Object} props
* @param {[tabData]} props.tabs array of tab data
* @param {Object} props.customProps common custom properties
*/
function TabComponents({ tabs, customProps = {} }) {
const [currentTabIndex, setCurrentTabIndex] = useState(0);
return (
tabs.length > 0 && (
<div className="TabComponents">
<div className="TabComponents_tabHeader">
<div className="TabComponents_tabHeader_selector">
<div className="dialog-separator-after">
<ul className="nav nav-tabs">
{tabs.map((tab, index) => {
const { name, hidden = false } = tab;
return (
!hidden && (
<li
key={index}
onClick={() => {
setCurrentTabIndex(index);
}}
className={classnames(
'nav-link',
index === currentTabIndex && 'active'
)}
data-cy={getDataCy(name)}
>
<button>{name}</button>
</li>
)
);
})}
</ul>
</div>
</div>
</div>
{tabs.map((tab, index) => {
const {
Component,
customProps: tabCustomProps,
hidden = false,
} = tab;
return (
!hidden && (
<div
key={index}
className={classnames(
'TabComponents_content',
index === currentTabIndex && 'active'
)}
>
<Component {...customProps} {...tabCustomProps} />
</div>
)
);
})}
</div>
)
);
}
TabComponents.propTypes = {
tabs: PropTypes.arrayOf(
PropTypes.shape({
name: PropTypes.string,
Component: PropTypes.any,
customProps: PropTypes.object,
hidden: PropTypes.bool,
})
),
customProps: PropTypes.object,
};
export { TabComponents };
@@ -0,0 +1,21 @@
@import './../../design/styles/common/navbar.styl'
.TabComponents
.TabComponents_tabHeader
display: flex
flex-direction: column
margin-left: -20px
margin-right: -20px
.TabComponents_tabHeader_selector
border-bottom: 3px solid black
padding: 0 25px
.TabComponents_content
min-height: 450px
display: none
&.active
display: flex
flex-direction: column
justify-content: space-between
@@ -0,0 +1,54 @@
import React from 'react';
import PropTypes from 'prop-types';
import './TabFooter.styl';
// In case translate is not passed
const translate = word => word;
function TabFooter({
onResetPreferences,
onSave,
onCancel,
hasErrors,
t = translate,
}) {
return (
<div className="footer">
<button
className="btn btn-danger pull-left"
data-cy="reset-default-btn"
onClick={onResetPreferences}
>
{t('Reset to Defaults')}
</button>
<div>
<div
onClick={onCancel}
data-cy="cancel-btn"
className="btn btn-default"
>
{t('Cancel')}
</div>
<button
className="btn btn-primary"
data-cy="save-btn"
disabled={hasErrors}
onClick={onSave}
>
{t('Save')}
</button>
</div>
</div>
);
}
TabFooter.propTypes = {
onResetPreferences: PropTypes.func,
onSave: PropTypes.func,
onCancel: PropTypes.func,
hasErrors: PropTypes.bool,
t: PropTypes.func,
};
export { TabFooter };
@@ -0,0 +1,11 @@
.footer
display: flex
flex-direction: row
justify-content: space-between
padding: 20px 20px 0 20px
margin: 0 -20px
border-top: 3px solid var(--primary-background-color)
div
button:last-child
margin-left: 10px
@@ -0,0 +1,27 @@
---
name: Tab Components
menu: Components
route: /components/tab-components
---
import { Playground, Props } from 'docz'
import { State } from 'react-powerplug'
import { TabComponents } from './../index.js'
// Data
import tabs from './tabs.js'
# Tab Components
## Basic usage
<Playground>
<TabComponents
tabs={tabs}
customProps={}
/>
</Playground>
## API
<Props of={TabComponents} />
@@ -0,0 +1,26 @@
export default tabs = [
{
name: 'tabName1',
Component: () => {
return <div>tab 1 Content</div>;
},
customProps: {},
hidden: false,
},
{
name: 'tabName2',
Component: () => {
return <div>tab 2 Content</div>;
},
customProps: {},
hidden: false,
},
{
name: 'tabName3',
Component: () => {
return <div>tab 3 Content</div>;
},
customProps: {},
hidden: true,
},
];
@@ -0,0 +1,2 @@
export { TabComponents } from './TabComponents';
export { TabFooter } from './TabFooter';
@@ -25,6 +25,20 @@ $headerRowHeight = 63px
text-align: left
flex: 1;
.tableListHeaderSelector
cursor: pointer
max-width: 30px
padding-left: 2px
align-items: stretch
flex: 1
justify-content: space-around
text-align: center
svg
fill: #c1d8e3
height: 63px
max-width: 11px
.numberOfItems
color: var(--ui-sky-blue)
float: right
@@ -1,57 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import LanguageSwitcher from '../languageSwitcher';
import i18n from '@ohif/i18n';
/**
* General Preferences tab
*/
/**
* General Preferences tab
* It renders the General Preferences content
*
* It stores current state and whenever it changes, component messages parent of new value (through function callback)
* @param {object} props component props
* @param {string} props.name Tab`s name
* @param {object} props.generalPreferences Data for initial state
* @param {function} props.onTabStateChanged Callback function to communicate parent in case its states changes
* @param {function} props.onTabErrorChanged Callback Function in case any error on tab
*/
function GeneralPreferences({
generalPreferences,
name,
onTabStateChanged,
onTabErrorChanged,
}) {
const { language = i18n.language } = generalPreferences;
const onLanguageChange = language => {
onTabStateChanged(name, {
generalPreferences: { ...generalPreferences, language },
});
};
return (
<div className="general-preferences-wrapper">
<div className="col-sm-3">
<label htmlFor="language-select" className="p-r-1">
Language
</label>
<LanguageSwitcher
language={language}
onLanguageChange={onLanguageChange}
/>
</div>
</div>
);
}
GeneralPreferences.propTypes = {
generalPreferences: PropTypes.any,
name: PropTypes.string,
onTabStateChanged: PropTypes.func,
onTabErrorChanged: PropTypes.func,
};
export { GeneralPreferences };
@@ -1,452 +0,0 @@
/* eslint-disable react-hooks/exhaustive-deps */
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import './HotKeysPreferences.styl';
import {
allowedKeys,
disallowedCombinations,
specialKeys,
} from './hotKeysConfig.js';
import isEqual from 'lodash.isequal';
const getKeysPressedArray = keyDownEvent => {
const keysPressedArray = [];
const { ctrlKey, altKey, shiftKey } = keyDownEvent;
if (ctrlKey && !altKey) {
keysPressedArray.push('ctrl');
}
if (shiftKey && !altKey) {
keysPressedArray.push('shift');
}
if (altKey && !ctrlKey) {
keysPressedArray.push('alt');
}
return keysPressedArray;
};
const findConflictingCommand = (
originalHotKeys,
currentCommandName,
currentHotKeys
) => {
let firstConflictingCommand = undefined;
for (const commandName in originalHotKeys) {
const toolHotKeys = originalHotKeys[commandName].keys;
if (
isEqual(toolHotKeys, currentHotKeys) &&
commandName !== currentCommandName
) {
firstConflictingCommand = originalHotKeys[commandName];
break;
}
}
return firstConflictingCommand;
};
/**
* Splits given keysObj into arrays. Each array item will be a representation of column
* @param {obj} keysObj objects to be splitted into columns
* @param {number} columnSize How many rows per column
*/
const getHotKeysArrayColumns = (keysObj = {}, columnSize) => {
if (isNaN(columnSize)) {
return keysObj;
}
const keys = Object.keys(keysObj);
const keysValues = Object.values(keysObj);
const keysLength = keys.length;
// Columns from left should be bigger;
let currentColumn = 0;
const dividedKeys = [];
for (
let it = 0;
it < keysLength;
it++, it % columnSize === 0 ? currentColumn++ : currentColumn
) {
if (!dividedKeys[currentColumn]) {
dividedKeys[currentColumn] = [];
}
dividedKeys[currentColumn][keys[it]] = keysValues[it];
}
return dividedKeys;
};
const NO_FIELD_ERROR_MESSAGE = undefined;
const formatPressedKeys = pressedKeysArray => pressedKeysArray.join('+');
const unFormatPressedKeys = (pressedKeysStr = '') => pressedKeysStr.split('+');
const inputValidators = (
commandName,
inputValue,
pressedKeys,
lastPressedKey,
originalHotKeys
) => {
let hasError = false;
let errorMessage = NO_FIELD_ERROR_MESSAGE;
const modifierValidator = ({ lastPressedKey }) => {
// Check if it has a valid modifier
const isModifier = ['ctrl', 'alt', 'shift'].includes(lastPressedKey);
if (isModifier) {
hasError = true;
errorMessage =
"It's not possible to define only modifier keys (ctrl, alt and shift) as a shortcut";
return {
hasError,
errorMessage,
};
}
};
const emptyValidator = ({ inputValue }) => {
if (!inputValue) {
hasError = true;
errorMessage = "Field can't be empty.";
return {
hasError,
errorMessage,
};
}
};
const conflictingValidator = ({
commandName,
pressedKeys,
originalHotKeys,
}) => {
const conflictingCommand = findConflictingCommand(
originalHotKeys,
commandName,
pressedKeys
);
if (conflictingCommand) {
hasError = true;
errorMessage = `"${conflictingCommand.label}" is already using the "${pressedKeys}" shortcut.`;
return {
hasError,
errorMessage,
};
}
};
const disallowedValidator = ({ inputValue, pressedKeys, lastPressedKey }) => {
const modifierCommand = formatPressedKeys(
pressedKeys.slice(0, pressedKeys.length - 1)
);
const disallowedCombination = disallowedCombinations[modifierCommand];
const hasDisallowedCombinations = disallowedCombination
? disallowedCombination.includes(lastPressedKey)
: false;
if (hasDisallowedCombinations) {
hasError = true;
errorMessage = `"${inputValue}" shortcut combination is not allowed`;
return {
hasError,
errorMessage,
};
}
};
const validators = [
emptyValidator,
modifierValidator,
conflictingValidator,
disallowedValidator,
];
for (const validator of validators) {
const validation = validator({
commandName,
inputValue,
pressedKeys,
lastPressedKey,
originalHotKeys,
});
if (validation && validation.hasError) {
return validation;
}
}
// validation has passed successfully
return {
hasError,
errorMessage,
};
};
/**
* HotKeysPreferencesRow
* Renders row for hotkey preference
* It stores current state and whenever it changes, component messages parent of new value (through function callback)
* @param {object} props component props
* @param {string} props.commandName command name associated to given row
* @param {string[]} props.hotkeys keys associated to given command
* @param {object} props.originalHotKeys original hotkeys values
* @param {function} props.onSuccessChanged Callback function to communicate parent in case its states changes
* @param {function} props.onFailureChanged Callback Function in case any error on row
*/
function HotKeyPreferencesRow({
commandName,
hotkeys,
label,
originalHotKeys,
tabError,
onSuccessChanged,
onFailureChanged,
}) {
const [inputValue, setInputValue] = useState(formatPressedKeys(hotkeys));
const [error, setError] = useState(false);
const [fieldErrorMessage, setFieldErrorMessage] = useState(
NO_FIELD_ERROR_MESSAGE
);
// reset error count if tab has no errors
useEffect(() => {
if (!tabError) {
setError(false);
setFieldErrorMessage(NO_FIELD_ERROR_MESSAGE);
setInputValue(formatPressedKeys(hotkeys));
}
}, [tabError]);
// update state values if props changes
useEffect(() => {
setInputValue(formatPressedKeys(hotkeys));
}, [hotkeys]);
const updateInputText = (keyDownEvent, displayPressedKey = false) => {
const pressedKeys = getKeysPressedArray(keyDownEvent);
if (displayPressedKey) {
const specialKeyName = specialKeys[keyDownEvent.which];
const keyName =
specialKeyName ||
keyDownEvent.key ||
String.fromCharCode(keyDownEvent.keyCode);
// ensure lowerCase
pressedKeys.push(keyName.toLowerCase());
}
setInputValue(formatPressedKeys(pressedKeys));
};
// validate input value
const validateInput = () => {
const pressedKeys = unFormatPressedKeys(inputValue);
const lastPressedKey = pressedKeys[pressedKeys.length - 1];
const {
hasError = false,
errorMessage = NO_FIELD_ERROR_MESSAGE,
} = inputValidators(
commandName,
inputValue,
pressedKeys,
lastPressedKey,
originalHotKeys
);
if (hasError) {
setInputValue('');
} else {
onSuccessChanged([inputValue]);
}
if (hasError !== error) {
setError(hasError);
}
setFieldErrorMessage(errorMessage);
};
useEffect(() => {
onFailureChanged(error);
}, [error]);
const onInputKeyDown = event => {
// Prevent ESC key from propagating and closing the modal
if (event.key === 'Escape') {
event.stopPropagation();
}
updateInputText(event, allowedKeys.includes(event.keyCode));
event.preventDefault();
};
return (
<tr key={commandName}>
<td className="text-right p-r-1">{label}</td>
<td width="200">
<label
className={`wrapperLabel ${
fieldErrorMessage !== undefined ? 'state-error' : ''
} `}
data-key="defaultTool"
>
<input
readOnly={true}
type="text"
value={inputValue}
className="form-control hotkey text-center"
onKeyDown={onInputKeyDown}
onBlur={validateInput}
/>
<span className="wrapperText" />
<span className="errorMessage">{fieldErrorMessage}</span>
</label>
</td>
</tr>
);
}
HotKeyPreferencesRow.propTypes = {
commandName: PropTypes.string.isRequired,
hotkeys: PropTypes.array.isRequired,
label: PropTypes.string.isRequired,
originalHotKeys: PropTypes.object.isRequired,
tabError: PropTypes.bool.isRequired,
onSuccessChanged: PropTypes.func.isRequired,
onFailureChanged: PropTypes.func.isRequired,
};
/**
* HotKeysPreferences tab
* It renders all hotkeys displayed into columns/rows
*
* It stores current state and whenever it changes, component messages parent of new value (through function callback)
* @param {object} props component props
* @param {string} props.name Tab`s name
* @param {object} props.hotkeyDefinitions Data for initial state
* @param {function} props.onTabStateChanged Callback function to communicate parent in case its states changes
* @param {function} props.onTabErrorChanged Callback Function in case any error on tab
*/
function HotKeysPreferences({
hotkeyDefinitions,
name,
tabError,
onTabStateChanged,
onTabErrorChanged,
}) {
const [tabState, setTabState] = useState(hotkeyDefinitions);
const [tabErrorCounter, setTabErrorCounter] = useState(0);
const [numColumns] = useState(2);
const [columnSize] = useState(() =>
Math.ceil(Object.keys(tabState || {}).length / numColumns)
);
const splittedHotKeys = getHotKeysArrayColumns(tabState, columnSize);
const onHotKeyChanged = (commandName, hotkeyDefinition, keys) => {
const newState = {
...tabState,
[commandName]: { ...hotkeyDefinition, keys },
};
setTabState(newState);
onTabStateChanged(name, { hotkeyDefinitions: newState });
};
const onErrorChanged = (toInc = true) => {
const increment = toInc ? 1 : -1;
const newValue = tabErrorCounter + increment;
if (newValue >= 0) {
setTabErrorCounter(newValue);
}
};
// reset error count if tab has no errors
useEffect(() => {
if (!tabError) {
setTabErrorCounter(0);
// update tab state
setTabState({ ...hotkeyDefinitions });
}
}, [tabError]);
// tell parent to update its state
useEffect(() => {
if (tabErrorCounter === 0) {
onTabErrorChanged(name, false);
}
if (tabErrorCounter === 1) {
onTabErrorChanged(name, true);
}
}, [tabErrorCounter]);
// update local state if parent updates
useEffect(() => {
setTabState({ ...hotkeyDefinitions });
}, [hotkeyDefinitions]);
return (
<div className="HotKeysPreferences">
{splittedHotKeys.length > 0
? splittedHotKeys.map((columnHotKeys, index) => {
return (
<div className="column" key={index}>
<table className="full-width">
<thead>
<tr>
<th className="text-right p-r-1">Function</th>
<th className="text-center">Shortcut</th>
</tr>
</thead>
<tbody>
{Object.entries(columnHotKeys).map(
hotkeyDefinitionTuple => (
<HotKeyPreferencesRow
key={hotkeyDefinitionTuple[0]}
commandName={hotkeyDefinitionTuple[0]}
hotkeys={hotkeyDefinitionTuple[1].keys}
label={hotkeyDefinitionTuple[1].label}
originalHotKeys={tabState}
tabError={tabError}
onSuccessChanged={keys =>
onHotKeyChanged(
hotkeyDefinitionTuple[0],
hotkeyDefinitionTuple[1],
keys
)
}
onFailureChanged={onErrorChanged}
></HotKeyPreferencesRow>
)
)}
</tbody>
</table>
</div>
);
})
: null}
</div>
);
}
HotKeysPreferences.propTypes = {
hotkeyDefinitions: PropTypes.any,
name: PropTypes.string,
tabError: PropTypes.bool,
onTabStateChanged: PropTypes.func,
onTabErrorChanged: PropTypes.func,
};
export { HotKeysPreferences };
@@ -1,12 +0,0 @@
.HotKeysPreferences
display: flex;
.column
width: 50%;
padding-right: 15px;
padding-left: 15px;
.column-full
width: 100%;
padding-right: 15px;
padding-left: 15px;
@@ -1,132 +0,0 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { HotKeysPreferences } from './HotKeysPreferences';
import { WindowLevelPreferences } from './WindowLevelPreferences';
import { GeneralPreferences } from './GeneralPreferences';
import './UserPreferences.styl';
export class UserPreferences extends Component {
static defaultProps = {
hotkeyDefinitions: [],
windowLevelData: {},
generalPreferences: {},
};
// TODO: Make this more generic. Tabs should not be restricted to these entries
static propTypes = {
hotkeyDefinitions: PropTypes.arrayOf(
PropTypes.shape({
commandName: PropTypes.string,
keys: PropTypes.arrayOf(PropTypes.string),
label: PropTypes.string,
})
).isRequired,
windowLevelData: PropTypes.object.isRequired,
generalPreferences: PropTypes.object.isRequired,
updatePropValue: PropTypes.func.isRequired,
};
state = {
tabIndex: 0,
};
tabClick(tabIndex) {
this.setState({ tabIndex });
}
renderHotkeysTab() {
return (
<form className="form-themed themed">
<div className="form-content">
<HotKeysPreferences
hotkeyDefinitions={this.props.hotkeyDefinitions}
/>
</div>
</form>
);
}
renderWindowLevelTab() {
if (this.props.windowLevelData) {
return (
<form className="form-themed themed">
<div className="form-content">
<WindowLevelPreferences
windowLevelData={this.props.windowLevelData}
/>
</div>
</form>
);
}
}
renderGeneralTab() {
return (
<form className="form-themed themed">
<div className="form-content">
<GeneralPreferences
generalPreferences={this.props.generalPreferences}
updatePropValue={this.props.updatePropValue}
/>
</div>
</form>
);
}
renderTabs(tabIndex) {
switch (tabIndex) {
case 0:
return this.renderHotkeysTab();
/* case 1:
return this.renderWindowLevelTab(); */
case 2:
return this.renderGeneralTab();
default:
break;
}
}
getTabClass(tabIndex) {
return tabIndex === this.state.tabIndex ? 'nav-link active' : 'nav-link';
}
render() {
return (
<div className="UserPreferences">
<div className="UserPreferences__selector">
<ul className="nav nav-tabs">
<li
onClick={() => {
this.tabClick(0);
}}
className={this.getTabClass(0)}
>
<button>Hotkeys</button>
</li>
{false && (
<li
onClick={() => {
this.tabClick(1);
}}
className={this.getTabClass(1)}
>
<button>Window Level</button>
</li>
)}
<li
onClick={() => {
this.tabClick(2);
}}
className={this.getTabClass(2)}
>
<button>General</button>
</li>
</ul>
</div>
{this.renderTabs(this.state.tabIndex)}
</div>
);
}
}
@@ -1,219 +0,0 @@
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import { useSnackbarContext } from '@ohif/ui';
import './UserPreferencesForm.styl';
import { useTranslation } from 'react-i18next';
// Tabs Component wrapper
import { UserPreferencesTabs } from './UserPreferencesTabs';
// Tabs
import { HotKeysPreferences } from './HotKeysPreferences';
import { WindowLevelPreferences } from './WindowLevelPreferences';
import { GeneralPreferences } from './GeneralPreferences';
/**
@typedef TabObject
@type {Object}
@property {string} name Name for given tab
@property {ReactComponent} Component React component for given tab.
@property {object} props Props State for given tab component
@property {boolean} [hidden] To hidden tab or not
*/
/**
* Create tabs obj.
* @returns {TabObject[]} Array of TabObjs.
*/
const createTabs = () => {
return [
{
name: 'Hotkeys',
Component: HotKeysPreferences,
props: {},
},
{
name: 'General',
Component: GeneralPreferences,
props: {},
},
{
name: 'Window Level',
Component: WindowLevelPreferences,
props: {},
hidden: true,
},
];
};
/**
* Main form component to render preferences tabs and buttons
* @param {object} props component props
* @param {string} props.name Tab`s name
* @param {object} props.hotkeyDefinitions Hotkeys Data
* @param {object} props.windowLevelData Window level data
* @param {function} props.onSave Callback function when saving
* @param {function} props.onClose Callback function when closing
* @param {function} props.onResetToDefaults Callback function when resetting
*/
function UserPreferencesForm({
onClose,
onSave,
onResetToDefaults,
windowLevelData,
hotkeyDefinitions,
generalPreferences,
hotkeysManager,
defaultLanguage,
hotkeyDefaults,
}) {
const [tabs, setTabs] = useState(createTabs());
const createTabsState = (
windowLevelData,
hotkeyDefinitions,
generalPreferences
) => {
return {
Hotkeys: { hotkeyDefinitions },
'Window Level': { windowLevelData },
General: { generalPreferences },
};
};
const [tabsState, setTabsState] = useState(
createTabsState(windowLevelData, hotkeyDefinitions, generalPreferences)
);
const [tabsError, setTabsError] = useState(
tabs.reduce((acc, tab) => {
acc[tab.name] = false;
return acc;
}, {})
);
const snackbar = useSnackbarContext();
const { t, ready: translationsAreReady } = useTranslation(
'UserPreferencesModal'
);
const onTabStateChanged = (tabName, newState) => {
setTabsState({ ...tabsState, [tabName]: newState });
};
const onTabErrorChanged = (tabName, hasError) => {
setTabsError({ ...tabsError, [tabName]: hasError });
};
const hasAnyError = () => {
return Object.values(tabsError).reduce((acc, value) => acc || value);
};
const onResetPreferences = () => {
const defaultHotKeyDefitions = {};
hotkeyDefaults.map(item => {
const { commandName, ...values } = item;
defaultHotKeyDefitions[commandName] = { ...values };
});
// update local state
setTabsState({
...tabsState,
Hotkeys: { hotkeyDefinitions: defaultHotKeyDefitions },
General: { generalPreferences: { language: defaultLanguage } },
});
// update tabs state
setTabs(createTabs(windowLevelData, hotkeyDefinitions, generalPreferences));
// reset errors
setTabsError(
tabs.reduce((acc, tab) => {
acc[tab.name] = false;
return acc;
}, {})
);
snackbar.show({
message: (
<div dangerouslySetInnerHTML={{ __html: t('ResetDefaultMessage') }} />
),
type: 'info',
});
};
const onSavePreferences = event => {
const toSave = Object.values(tabsState).reduce((acc, tabState) => {
return { ...acc, ...tabState };
}, {});
onSave(toSave);
snackbar.show({
message: t('SaveMessage'),
type: 'success',
});
};
// update local state if prop values changes
useEffect(() => {
setTabsState(
createTabsState(windowLevelData, hotkeyDefinitions, generalPreferences)
);
}, [windowLevelData, hotkeyDefinitions, generalPreferences]);
return translationsAreReady ? (
<div className="UserPreferencesForm">
<UserPreferencesTabs
tabs={tabs}
tabsState={tabsState}
tabsError={tabsError}
onTabStateChanged={onTabStateChanged}
onTabErrorChanged={onTabErrorChanged}
/>
<div className="footer">
<button
className="btn btn-danger pull-left"
data-cy="reset-default-btn"
onClick={onResetPreferences}
>
{t('Reset to Defaults')}
</button>
<div>
<div
onClick={onClose}
data-cy="cancel-btn"
className="btn btn-default"
>
{t('Cancel')}
</div>
<button
className="btn btn-primary"
data-cy="save-btn"
disabled={hasAnyError()}
onClick={onSavePreferences}
>
{t('Save')}
</button>
</div>
</div>
</div>
) : null;
}
UserPreferencesForm.propTypes = {
onClose: PropTypes.func,
onSave: PropTypes.func,
onResetToDefaults: PropTypes.func,
windowLevelData: PropTypes.object,
hotkeyDefinitions: PropTypes.object,
generalPreferences: PropTypes.object,
hotkeysManager: PropTypes.object,
defaultLanguage: PropTypes.string,
hotkeyDefaults: PropTypes.array,
};
export { UserPreferencesForm };
@@ -1,23 +0,0 @@
@import './../../design/styles/common/navbar.styl'
@import './../../design/styles/common/global.styl'
@import './../../design/styles/common/modal.styl'
@import './../../design/styles/common/button.styl'
.close
float: right;
font-size: 21px;
font-weight: 700;
line-height: 1;
color: #000;
text-shadow: 0 1px 0 #fff;
opacity: .2;
.UserPreferencesForm
.footer
display: flex
flex-direction: row
justify-content: space-between
div
button:last-child
margin-left: 10px
@@ -1,107 +0,0 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import './UserPreferencesTabs.styl';
/**
* Render tab component
* @param {object} tab TabObject containing tab data
* @param {function} onTabStateChanged Callback Function in case tab changes its state
* @param {function} onTabErrorChanged Callback Function in case any error on tab
*/
const renderTab = (
tab = {},
tabsState,
tabsError,
onTabStateChanged,
onTabErrorChanged
) => {
const { props, Component, name, hidden = false } = tab;
const tabState = tabsState[name];
const tabError = tabsError[name];
return !hidden ? (
<form className="form-themed themed tabs">
<div className="form-content">
<Component
key={name}
{...tabState}
{...props}
name={name}
tabError={tabError}
onTabStateChanged={onTabStateChanged}
onTabErrorChanged={onTabErrorChanged}
></Component>
</div>
</form>
) : null;
};
const renderTabsHeader = (tabs, activeTabIndex, onHeaderChanged) => {
return tabs.length > 0
? tabs.map((tab, index) => {
const { name, hidden = false } = tab;
const cypressSelectorId = name.toLowerCase();
const tabClass =
index === activeTabIndex ? 'nav-link active' : 'nav-link';
return !hidden ? (
<li
key={name}
onClick={() => {
onHeaderChanged(index);
}}
className={tabClass}
data-cy={cypressSelectorId}
>
<button>{name}</button>
</li>
) : null;
})
: null;
};
/**
* Component to render tabs based on currentActiveTabIndex
*
* In case any tab changes its state this current component tells parent through function callback
* @param {object} props Component props
*/
function UserPreferencesTabs({
tabs,
tabsState,
tabsError,
onTabStateChanged,
onTabErrorChanged,
}) {
const [activeTabIndex, setActiveTabIndex] = useState(0);
return (
<div className="UserPreferencesTabs">
<div className="UserPreferencesTabs__selector">
<div className="dialog-separator-after">
<ul className="nav nav-tabs">
{renderTabsHeader(tabs, activeTabIndex, setActiveTabIndex)}
</ul>
</div>
</div>
{renderTab(
tabs[activeTabIndex],
tabsState,
tabsError,
onTabStateChanged,
onTabErrorChanged
)}
</div>
);
}
UserPreferencesTabs.propTypes = {
tabs: PropTypes.array.isRequired,
tabsState: PropTypes.object.isRequired,
tabsError: PropTypes.object.isRequired,
onTabStateChanged: PropTypes.func.isRequired,
onTabErrorChanged: PropTypes.func.isRequired,
};
export { UserPreferencesTabs };
@@ -1,29 +0,0 @@
@import './../../design/styles/common/form.styl'
@import './../../design/styles/common/navbar.styl'
@import './../../design/styles/common/state.styl'
@import './../../design/styles/common/global.styl'
.UserPreferencesTabs
display: flex
flex-direction: column
&__selector
border-bottom: 3px solid black
.errorMessage
color: var(--state-error-text)
font-size: 10px
text-transform: uppercase;
.form-content
border-bottom: 3px solid var(--primary-background-color)
margin-bottom: 20px
margin-left: -20px
margin-right: -20px
max-height: 70vh
overflow-y: auto
padding: 20px
min-height: 500px
.popover
width: 300px
@@ -1,132 +0,0 @@
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import './WindowLevelPreferences.styl';
/**
* WindowLevelPreferencesRow
* Renders row for window level preference
* It stores current state and whenever it changes, component messages parent of new value (through function callback)
* @param {object} props component props
* @param {string} props.description description for given preset
* @param {number} props.window window value
* @param {number} props.level level value
* @param {string} props.rowName name of given row to identify it
* @param {function} props.onSuccessChanged Callback function to communicate parent in case its states changes
*/
function WindowLevelPreferencesRow({
description,
window,
level,
rowName,
onSuccessChanged,
// onFailureChanged
}) {
const [rowState, setRowState] = useState({ description, window, level });
const onInputChanged = (event, name) => {
const newValue = event.target.value;
setRowState({ ...rowState, [name]: newValue });
};
useEffect(() => {
onSuccessChanged(rowName, rowState);
}, [rowState]);
const renderTd = (value, name, type) => {
return (
<td className="p-r-1">
<label className="wrapperLabel">
<input
value={value}
type={type}
className="form-control"
onChange={event => {
onInputChanged(event, name);
}}
/>
</label>
</td>
);
};
return (
<tr key={rowName}>
<td className="p-r-1 text-center">{rowName}</td>
{renderTd(rowState.description, 'description', 'text')}
{renderTd(rowState.window, 'window', 'number')}
{renderTd(rowState.level, 'level', 'number')}
</tr>
);
}
WindowLevelPreferencesRow.propTypes = {
description: PropTypes.string.isRequired,
window: PropTypes.number.isRequired,
level: PropTypes.number.isRequired,
rowName: PropTypes.string.isRequired,
onSuccessChanged: PropTypes.func.isRequired,
//onFailureChanged: PropTypes.func.isRequired,
};
/**
* WindowLevelPreferences tab
* It renders all window level presets
*
* It stores current state and whenever it changes, component messages parent of new value (through function callback)
* @param {object} props component props
* @param {string} props.name Tab`s name
* @param {object} props.windowLevelData Data for initial state
* @param {function} props.onTabStateChanged Callback function to communicate parent in case its states changes
*/
function WindowLevelPreferences({
windowLevelData,
name,
onTabStateChanged /*onTabErrorChanged*/,
}) {
const [tabState, setTabState] = useState(windowLevelData);
// TODO to be used once error handling is implemented
//const [tabError, setTabError] = useState(false);
const onWindowLevelChanged = (key, state) => {
setTabState({ ...tabState, [key]: state });
};
// tell parent to update its state
useEffect(() => {
onTabStateChanged(name, { windowLevelData: tabState });
}, [tabState]);
return (
<table className="full-width">
<thead>
<tr>
<th className="p-x-1 text-center presetIndex">Preset</th>
<th className="p-x-1">Description</th>
<th className="p-x-1">Window</th>
<th className="p-x-1">Level</th>
</tr>
</thead>
<tbody>
{Object.keys(tabState).map(objKey => (
<WindowLevelPreferencesRow
onSuccessChanged={onWindowLevelChanged}
rowName={objKey}
key={objKey}
description={tabState[objKey].description}
window={tabState[objKey].window}
level={tabState[objKey].level}
></WindowLevelPreferencesRow>
))}
</tbody>
</table>
);
}
WindowLevelPreferences.propTypes = {
windowLevelData: PropTypes.object.isRequired,
name: PropTypes.string.isRequired,
onTabStateChanged: PropTypes.func.isRequired,
//onTabErrorChanged: PropTypes.func.isRequired,
};
export { WindowLevelPreferences };
@@ -1,4 +0,0 @@
@import './UserPreferencesTabs.styl'
.presetIndex
padding: 0px 10px 0px 10px
@@ -1,14 +0,0 @@
export default {
currentLanguage: 'en',
languages: [
{
value: 'en',
label: 'English',
},
{
value: 'es',
label: 'Spanish',
},
],
onChange: language => {},
};
@@ -1,75 +0,0 @@
export default {
defaultTool: { label: 'Default Tool', keys: ['ESC'], column: 0 },
zoom: { label: 'Zoom', keys: ['Z'], column: 0 },
wwwc: { label: 'W/L', keys: ['W'], column: 0 },
pan: { label: 'Pan', keys: ['P'], column: 0 },
angle: { label: 'Angle measurement', keys: ['A'], column: 0 },
stackScroll: { label: 'Scroll stack', keys: ['S'], column: 0 },
magnify: { label: 'Magnify', keys: ['M'], column: 0 },
length: { label: 'Length measurement', keys: [''], column: 0 },
annotate: { label: 'Annotate', keys: [''], column: 0 },
dragProbe: { label: 'Pixel probe', keys: [''], column: 0 },
ellipticalRoi: { label: 'Elliptical ROI', keys: [''], column: 0 },
rectangleRoi: { label: 'Rectangle ROI', keys: [''], column: 0 },
// Viewport hotkeys
flipH: { label: 'Flip Horizontally', keys: ['H'], column: 0 },
flipV: { label: 'Flip Vertically', keys: ['V'], column: 0 },
rotateR: { label: 'Rotate Right', keys: ['R'], column: 0 },
rotateL: { label: 'Rotate Left', keys: ['L'], column: 0 },
invert: { label: 'Invert', keys: ['I'], column: 0 },
zoomIn: { label: 'Zoom In', keys: [''], column: 0 },
zoomOut: { label: 'Zoom Out', keys: [''], column: 0 },
zoomToFit: { label: 'Zoom to Fit', keys: [''], column: 0 },
resetViewport: { label: 'Reset', keys: [''], column: 0 },
clearTools: { label: 'Clear Tools', keys: [''], column: 0 },
// 2nd column
// Viewport navigation hotkeys
scrollDown: { label: 'Scroll Down', keys: ['DOWN'], column: 1 },
scrollUp: { label: 'Scroll Up', keys: ['UP'], column: 1 },
scrollLastImage: { label: 'Scroll to Last Image', keys: ['END'], column: 1 },
scrollFirstImage: {
label: 'Scroll to First Image',
keys: ['HOME'],
column: 1,
},
previousDisplaySet: {
label: 'Previous Series',
keys: ['PAGEUP'],
column: 1,
},
nextDisplaySet: { label: 'Next Series', keys: ['PAGEDOWN'], column: 1 },
nextPanel: { label: 'Next Image Viewport', keys: ['RIGHT'], column: 1 },
previousPanel: {
label: 'Previous Image Viewport',
keys: ['LEFT'],
column: 1,
},
// Miscellaneous hotkeys
toggleOverlayTags: {
label: 'Toggle Image Info Overlay',
keys: ['O'],
column: 1,
},
toggleCinePlay: { label: 'Play/Pause Cine', keys: ['SPACE'], column: 1 },
toggleCineDialog: {
label: 'Show/Hide Cine Controls',
keys: [''],
column: 1,
},
// Preset hotkeys
WLPreset0: { label: 'W/L Preset 0 (Soft Tissue)', keys: ['1'], column: 1 },
WLPreset1: { label: 'W/L Preset 1 (Lung)', keys: ['2'], column: 1 },
WLPreset2: { label: 'W/L Preset 2 (Liver)', keys: ['3'], column: 1 },
WLPreset3: { label: 'W/L Preset 3 (Bone)', keys: ['4'], column: 1 },
WLPreset4: { label: 'W/L Preset 4 (Brain)', keys: ['5'], column: 1 },
WLPreset5: { label: 'W/L Preset 5', keys: ['6'], column: 1 },
WLPreset6: { label: 'W/L Preset 6', keys: ['7'], column: 1 },
WLPreset7: { label: 'W/L Preset 7', keys: ['8'], column: 1 },
WLPreset8: { label: 'W/L Preset 8', keys: ['9'], column: 1 },
WLPreset9: { label: 'W/L Preset 0', keys: ['0'], column: 1 },
};
@@ -1,54 +0,0 @@
---
name: User Preferences Form
menu: Components
route: /components/user-preferences-form
---
import { Playground, Props } from 'docz'
import { State } from 'react-powerplug'
import { UserPreferencesForm } from './../index.js'
import NameSpace from '../../../__docs__/NameSpace'
//
import windowLevelDefaults from './windowLevelDefaults.js'
import hotkeyDefaults from './hotkeyDefaults.js'
# User Preferences Form
## Basic usage
<Playground>
<State initial={{
isOpen: false,
windowLevelData: windowLevelDefaults,
hotkeyDefinitions: hotkeyDefaults,
}}>
{({ state, setState }) => (
<React.Fragment>
<button
className="btn btn-primary"
type="button"
onClick={ () => setState({ isOpen: true }) }
>
Open user preferences
</button>
<UserPreferencesForm
{...state}
onCancel={() => setState({ isOpen: false })}
onSave={() => alert('on save')}
onResetToDefaults={() => alert('on reset')}
/>
</React.Fragment>
)}
</State>
</Playground>
## API
<Props of={UserPreferencesForm} />
## Translation Namespace
<NameSpace name="UserPreferencesForm" />
@@ -1,13 +0,0 @@
export default {
0: { description: 'Soft tissue', window: 400, level: 40 },
1: { description: 'Lung', window: 1500, level: -600 },
2: { description: 'Liver', window: 150, level: 90 },
3: { description: 'Bone', window: 2500, level: 480 },
4: { description: 'Brain', window: 80, level: 40 },
5: { description: '', window: '', level: '' },
6: { description: '', window: '', level: '' },
7: { description: '', window: '', level: '' },
8: { description: '', window: '', level: '' },
9: { description: '', window: '', level: '' },
10: { description: '', window: '', level: '' },
};
@@ -1,4 +0,0 @@
export { UserPreferencesTabs } from './UserPreferencesTabs.js';
export { AboutContent } from '../content/aboutContent/AboutContent.js';
export { UserPreferencesForm } from './UserPreferencesForm.js';
export { GeneralPreferences } from './GeneralPreferences.js';
+8 -4
View File
@@ -19,13 +19,15 @@ import {
TableList,
TableListItem,
Thumbnail,
TabComponents,
TabFooter,
HotkeyField,
LanguageSwitcher,
TableSearchFilter,
TablePagination,
ToolbarSection,
Tooltip,
AboutContent,
UserPreferences,
UserPreferencesForm,
OHIFModal,
} from './components';
import { useDebounce, useMedia } from './hooks';
@@ -98,6 +100,10 @@ export {
TableList,
TableListItem,
Thumbnail,
TabComponents,
TabFooter,
HotkeyField,
LanguageSwitcher,
TableSearchFilter,
TablePagination,
Toolbar,
@@ -105,8 +111,6 @@ export {
ToolbarSection,
Tooltip,
AboutContent,
UserPreferences,
UserPreferencesForm,
ViewerbaseDragDropContext,
SnackbarProvider,
useSnackbarContext,
+60
View File
@@ -3,6 +3,66 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [3.7.2](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.7.1...@ohif/viewer@3.7.2) (2020-02-29)
### Bug Fixes
* prevent the native context menu from appearing when right-clicking on a measurement or angle (https://github.com/OHIF/Viewers/issues/1406) ([#1469](https://github.com/OHIF/Viewers/issues/1469)) ([9b3be9b](https://github.com/OHIF/Viewers/commit/9b3be9b0c082c9a5b62f2a40f42e59381860fe73))
## [3.7.1](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.7.0...@ohif/viewer@3.7.1) (2020-02-21)
**Note:** Version bump only for package @ohif/viewer
# [3.7.0](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.6.3...@ohif/viewer@3.7.0) (2020-02-20)
### Features
* [#1342](https://github.com/OHIF/Viewers/issues/1342) - Window level tab ([#1429](https://github.com/OHIF/Viewers/issues/1429)) ([ebc01a8](https://github.com/OHIF/Viewers/commit/ebc01a8ca238d5a3437b44d81f75aa8a5e8d0574))
## [3.6.3](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.6.2...@ohif/viewer@3.6.3) (2020-02-14)
### Bug Fixes
* Creating 2 commands to activate zoom tool and also to move between displaySets ([#1446](https://github.com/OHIF/Viewers/issues/1446)) ([06a4af0](https://github.com/OHIF/Viewers/commit/06a4af06faaecf6fa06ccd90cdfa879ee8d53053))
## [3.6.2](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.6.1...@ohif/viewer@3.6.2) (2020-02-12)
### Bug Fixes
* Combined Hotkeys for special characters ([#1233](https://github.com/OHIF/Viewers/issues/1233)) ([2f30e7a](https://github.com/OHIF/Viewers/commit/2f30e7a821a238144c49c56f37d8e5565540b4bd))
## [3.6.1](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.6.0...@ohif/viewer@3.6.1) (2020-02-10)
**Note:** Version bump only for package @ohif/viewer
# [3.6.0](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.5.1...@ohif/viewer@3.6.0) (2020-02-10)
@@ -30,9 +30,8 @@ describe('OHIF User Preferences', () => {
});
it('checks translation by selecting Spanish language', function() {
cy.get('@userPreferencesGeneralTab')
.click()
.should('have.class', 'active');
cy.changePreferencesTab('@userPreferencesGeneralTab');
cy.get('@userPreferencesGeneralTab').should('have.class', 'active');
// Language dropdown should be displayed
cy.get('#language-select').should('be.visible');
@@ -65,14 +64,14 @@ describe('OHIF User Preferences', () => {
cy.get('[data-cy="options-menu"]').click();
});
it('checks if user can cancel the language selection and application will be in English', function() {
it('checks if user can cancel the language selection and application will be in "English (USA)"', function() {
// Set language to English and save
cy.setLanguage('English');
cy.setLanguage('English (USA)');
// Set language to Spanish and cancel
cy.setLanguage('Spanish', false);
// Header should be kept in English
// Header should be kept in "English (USA)"
cy.get('.research-use')
.scrollIntoView()
.should('have.text', 'INVESTIGATIONAL USE ONLY');
@@ -93,7 +92,7 @@ describe('OHIF User Preferences', () => {
cy.get('[data-cy="options-menu"]').click();
});
it('checks if user can restore to default the language selection and application will be in English', function() {
it('checks if user can restore to default the language selection and application will be in "English (USA)"', function() {
// Set language to Spanish
cy.setLanguage('Spanish');
@@ -101,7 +100,7 @@ describe('OHIF User Preferences', () => {
cy.openPreferences();
// Go to general tab
cy.get('@userPreferencesGeneralTab').click();
cy.changePreferencesTab('@userPreferencesGeneralTab');
cy.get('@restoreBtn')
.scrollIntoView()
@@ -112,12 +111,12 @@ describe('OHIF User Preferences', () => {
.scrollIntoView()
.click();
// Header should be in English
// Header should be in "English (USA)"
cy.get('.research-use')
.scrollIntoView()
.should('have.text', 'INVESTIGATIONAL USE ONLY');
// Options menu should be in English
// Options menu should be in "English (USA)"
cy.get('[data-cy="options-menu"]')
.should('have.text', 'Options')
.click();
@@ -135,9 +134,8 @@ describe('OHIF User Preferences', () => {
it('checks if Preferences set in Study List Page will be consistent on Viewer Page', function() {
// Go go hotkeys tab
cy.get('@userPreferencesHotkeysTab')
.click()
.should('have.class', 'active');
cy.changePreferencesTab('@userPreferencesHotkeysTab');
cy.get('@userPreferencesHotkeysTab').should('have.class', 'active');
// Set new hotkey for 'Rotate Right' function
cy.setNewHotkeyShortcutOnUserPreferencesModal('Rotate Right', '{shift}Q');
@@ -150,7 +148,7 @@ describe('OHIF User Preferences', () => {
cy.openPreferences();
// Go to General tab
cy.get('@userPreferencesGeneralTab').click();
cy.changePreferencesTab('@userPreferencesGeneralTab');
// Set language to Spanish
cy.setLanguage('Spanish');
@@ -221,9 +219,8 @@ describe('OHIF User Preferences', () => {
});
it('checks translation by selecting Spanish language', function() {
cy.get('@userPreferencesGeneralTab')
.click()
.should('have.class', 'active');
cy.changePreferencesTab('@userPreferencesGeneralTab');
cy.get('@userPreferencesGeneralTab').should('have.class', 'active');
// Language dropdown should be displayed
cy.get('#language-select').should('be.visible');
@@ -256,14 +253,14 @@ describe('OHIF User Preferences', () => {
cy.get('[data-cy="options-menu"]').click();
});
it('checks if user can cancel the language selection and application will be in English', function() {
it('checks if user can cancel the language selection and application will be in "English (USA)"', function() {
// Set language to English and save
cy.setLanguage('English');
cy.setLanguage('English (USA)');
// Set language to Spanish and cancel
cy.setLanguage('Spanish', false);
// Header should be kept in English
// Header should be kept in "English (USA)"
cy.get('.research-use')
.scrollIntoView()
.should('have.text', 'INVESTIGATIONAL USE ONLY');
@@ -283,10 +280,9 @@ describe('OHIF User Preferences', () => {
cy.get('[data-cy="options-menu"]').click();
});
it('checks if user can restore to default the language selection and application will be in English', function() {
cy.get('@userPreferencesGeneralTab')
.click()
.should('have.class', 'active');
it('checks if user can restore to default the language selection and application will be in "English (USA)', function() {
cy.changePreferencesTab('@userPreferencesGeneralTab');
cy.get('@userPreferencesGeneralTab').should('have.class', 'active');
// Language dropdown should be displayed
cy.get('#language-select').should('be.visible');
@@ -298,7 +294,7 @@ describe('OHIF User Preferences', () => {
cy.openPreferences();
// Go to general tab
cy.get('@userPreferencesGeneralTab').click();
cy.changePreferencesTab('@userPreferencesGeneralTab');
cy.get('@restoreBtn')
.scrollIntoView()
@@ -308,12 +304,12 @@ describe('OHIF User Preferences', () => {
.scrollIntoView()
.click();
// Header should be in English
// Header should be in "English (USA)""
cy.get('.research-use')
.scrollIntoView()
.should('have.text', 'INVESTIGATIONAL USE ONLY');
// Options menu should be in English
// Options menu should be in "English (USA)"
cy.get('[data-cy="options-menu"]')
.should('have.text', 'Options')
.click();
@@ -330,9 +326,8 @@ describe('OHIF User Preferences', () => {
it('checks new hotkeys for "Rotate Right" and "Rotate Left"', function() {
// Go go hotkeys tab
cy.get('@userPreferencesHotkeysTab')
.click()
.should('have.class', 'active');
cy.changePreferencesTab('@userPreferencesHotkeysTab');
cy.get('@userPreferencesHotkeysTab').should('have.class', 'active');
// Set new hotkey for 'Rotate Right' function
cy.setNewHotkeyShortcutOnUserPreferencesModal(
@@ -360,24 +355,17 @@ describe('OHIF User Preferences', () => {
});
it('checks new hotkeys for "Next" and "Previous" Image on Viewport', function() {
// Go go hotkeys tab
cy.get('@userPreferencesHotkeysTab')
.click()
.should('have.class', 'active');
// Set new hotkey for 'Next Image Viewport' function
// Update hotkeys for 'Next/Previous Viewport'
cy.changePreferencesTab('@userPreferencesHotkeysTab');
cy.get('@userPreferencesHotkeysTab').should('have.class', 'active');
cy.setNewHotkeyShortcutOnUserPreferencesModal(
'Next Image Viewport',
'Next Viewport',
'{shift}{rightarrow}'
);
// Set new hotkey for 'Previous Image Viewport' function
cy.setNewHotkeyShortcutOnUserPreferencesModal(
'Previous Image Viewport',
'Previous Viewport',
'{shift}{leftarrow}'
);
// Save new hotkeys
cy.get('@saveBtn')
.scrollIntoView()
.click();
@@ -386,20 +374,25 @@ describe('OHIF User Preferences', () => {
cy.setLayout(3, 1);
cy.waitViewportImageLoading();
// Rotate Right and Invert colors on Viewport #1
cy.get('body').type('RI');
// Check that image was rotated
// Reset, Rotate Right and Invert colors on Viewport #1
cy.get('body').type(' ');
cy.get('body').type('r');
cy.get('body').type('i');
// Shift active viewport to next
// Reset, Rotate Left and Invert colors on Viewport #2
cy.get('body').type('{shift}{rightarrow}');
cy.get('body').type(' ');
cy.get('body').type('l');
cy.get('body').type('i');
// Verify 1st viewport was rotated
cy.get('@viewportInfoMidTop').should('contains.text', 'R');
//Move to Next Viewport
cy.get('body').type('{shift}{rightarrow}');
// Rotate Left and Invert colors on Viewport #2
cy.get('body').type('LI');
// Get overlay information from viewport #2
// Verify 2nd viewport was rotated
cy.get(
':nth-child(2) > .viewport-wrapper > .viewport-element > .ViewportOrientationMarkers.noselect > .top-mid.orientation-marker'
).as('viewport2InfoMidTop');
// Check that image was rotated
cy.get('@viewport2InfoMidTop').should('contains.text', 'P');
//Move to Previous Viewport
@@ -417,24 +410,18 @@ describe('OHIF User Preferences', () => {
it('checks error message when duplicated hotkeys are inserted', function() {
// Go go hotkeys tab
cy.get('@userPreferencesHotkeysTab').click();
cy.changePreferencesTab('@userPreferencesHotkeysTab');
// Set duplicated hotkey for 'Rotate Right' function
cy.setNewHotkeyShortcutOnUserPreferencesModal(
'Rotate Right',
'{rightarrow}'
);
cy.setNewHotkeyShortcutOnUserPreferencesModal('Rotate Right', '{i}');
// Check error message
cy.get('.HotKeysPreferences').within(() => {
cy.get('.HotkeysPreferences').within(() => {
cy.contains('Rotate Right') // label we're looking for
.parent()
.find('.errorMessage')
.find('.preferencesInputErrorMessage')
.as('errorMsg')
.should(
'have.text',
'"Next Image Viewport" is already using the "right" shortcut.'
);
.should('have.text', '"Invert" is already using the "i" shortcut.');
});
//Cancel hotkeys
cy.get('@cancelBtn')
@@ -444,16 +431,16 @@ describe('OHIF User Preferences', () => {
it('checks error message when invalid hotkey is inserted', function() {
// Go go hotkeys tab
cy.get('@userPreferencesHotkeysTab').click();
cy.changePreferencesTab('@userPreferencesHotkeysTab');
// Set invalid hotkey for 'Rotate Right' function
cy.setNewHotkeyShortcutOnUserPreferencesModal('Rotate Right', '{ctrl}Z');
// Check error message
cy.get('.HotKeysPreferences').within(() => {
cy.get('.HotkeysPreferences').within(() => {
cy.contains('Rotate Right') // label we're looking for
.parent()
.find('.errorMessage')
.find('.preferencesInputErrorMessage')
.as('errorMsg')
.should('have.text', '"ctrl+z" shortcut combination is not allowed');
});
@@ -466,15 +453,15 @@ describe('OHIF User Preferences', () => {
it('checks error message when only modifier keys are inserted', function() {
// Go go hotkeys tab
cy.get('@userPreferencesHotkeysTab').click();
cy.changePreferencesTab('@userPreferencesHotkeysTab');
// Set invalid modifier key: ctrl
cy.setNewHotkeyShortcutOnUserPreferencesModal('Zoom Out', '{ctrl}');
// Check error message
cy.get('.HotKeysPreferences').within(() => {
cy.get('.HotkeysPreferences').within(() => {
cy.contains('Zoom Out') // label we're looking for
.parent()
.find('.errorMessage')
.find('.preferencesInputErrorMessage')
.as('errorMsg')
.should(
'have.text',
@@ -506,7 +493,7 @@ describe('OHIF User Preferences', () => {
it('checks if user can cancel changes made on User Preferences Hotkeys tab', function() {
// Go go hotkeys tab
cy.get('@userPreferencesHotkeysTab').click();
cy.changePreferencesTab('@userPreferencesHotkeysTab');
// Set new hotkey for 'Rotate Right' function
cy.setNewHotkeyShortcutOnUserPreferencesModal(
@@ -523,7 +510,7 @@ describe('OHIF User Preferences', () => {
cy.openPreferences();
//Check that hotkey for 'Rotate Right' function was not changed
cy.get('.HotKeysPreferences').within(() => {
cy.get('.HotkeysPreferences').within(() => {
cy.contains('Rotate Right') // label we're looking for
.parent()
.find('input')
@@ -534,7 +521,7 @@ describe('OHIF User Preferences', () => {
it('checks if user can reset to default values on User Preferences Hotkeys tab', function() {
// Go go hotkeys tab
cy.get('@userPreferencesHotkeysTab').click();
cy.changePreferencesTab('@userPreferencesHotkeysTab');
// Set new hotkey for 'Rotate Right' function
cy.setNewHotkeyShortcutOnUserPreferencesModal(
@@ -559,7 +546,7 @@ describe('OHIF User Preferences', () => {
cy.openPreferences();
//Check that hotkey for 'Rotate Right' function was not changed
cy.get('.HotKeysPreferences').within(() => {
cy.get('.HotkeysPreferences').within(() => {
cy.contains('Rotate Right') // label we're looking for
.parent()
.find('input')
@@ -21,9 +21,8 @@ describe('Visual Regression - OHIF User Preferences', () => {
});
it('checks translation by selecting Spanish language', function() {
cy.get('@userPreferencesGeneralTab')
.click()
.should('have.class', 'active');
cy.changePreferencesTab('@userPreferencesGeneralTab');
cy.get('@userPreferencesGeneralTab').should('have.class', 'active');
// Language dropdown should be displayed
cy.get('#language-select').should('be.visible');
@@ -76,9 +75,8 @@ describe('Visual Regression - OHIF User Preferences', () => {
});
it('checks translation by selecting Spanish language', function() {
cy.get('@userPreferencesGeneralTab')
.click()
.should('have.class', 'active');
cy.changePreferencesTab('@userPreferencesGeneralTab');
cy.get('@userPreferencesGeneralTab').should('have.class', 'active');
// Visual comparison
cy.percyCanvasSnapshot(
@@ -102,9 +100,8 @@ describe('Visual Regression - OHIF User Preferences', () => {
});
it('checks if user can restore to default the language selection and application will be in English', function() {
cy.get('@userPreferencesGeneralTab')
.click()
.should('have.class', 'active');
cy.changePreferencesTab('@userPreferencesGeneralTab');
cy.get('@userPreferencesGeneralTab').should('have.class', 'active');
// Set language to Spanish
cy.setLanguage('Spanish');
@@ -113,7 +110,7 @@ describe('Visual Regression - OHIF User Preferences', () => {
cy.openPreferences();
// Go to general tab
cy.get('@userPreferencesGeneralTab').click();
cy.changePreferencesTab('@userPreferencesGeneralTab');
cy.get('@restoreBtn')
.scrollIntoView()
@@ -139,9 +136,8 @@ describe('Visual Regression - OHIF User Preferences', () => {
it('checks new hotkeys for "Next" and "Previous" Image on Viewport', function() {
// Go go hotkeys tab
cy.get('@userPreferencesHotkeysTab')
.click()
.should('have.class', 'active');
cy.changePreferencesTab('@userPreferencesHotkeysTab');
cy.get('@userPreferencesHotkeysTab').should('have.class', 'active');
// Set new hotkey for 'Next Image Viewport' function
cy.setNewHotkeyShortcutOnUserPreferencesModal(
+8 -3
View File
@@ -84,7 +84,12 @@ export function initPreferencesModalAliases() {
cy.get('.OHIFModal').as('preferencesModal');
cy.get('[data-cy="hotkeys"]').as('userPreferencesHotkeysTab');
cy.get('[data-cy="general"]').as('userPreferencesGeneralTab');
cy.get('[data-cy="reset-default-btn"]').as('restoreBtn');
cy.get('[data-cy="cancel-btn"]').as('cancelBtn');
cy.get('[data-cy="save-btn"]').as('saveBtn');
initPreferencesModalFooterBtnAliases();
}
//Creating aliases for User Preferences modal
export function initPreferencesModalFooterBtnAliases() {
cy.get('.active [data-cy="reset-default-btn"]').as('restoreBtn');
cy.get('.active [data-cy="cancel-btn"]').as('cancelBtn');
cy.get('.active [data-cy="save-btn"]').as('saveBtn');
}
+18 -11
View File
@@ -9,6 +9,7 @@ import {
initStudyListAliasesOnDesktop,
initStudyListAliasesOnTablet,
initPreferencesModalAliases,
initPreferencesModalFooterBtnAliases,
} from './aliases.js';
// ***********************************************
@@ -169,13 +170,13 @@ Cypress.Commands.add(
);
Cypress.Commands.add('expectMinimumThumbnails', (seriesToWait = 1) => {
cy.get('[data-cy=thumbnail-list]', { timeout: 20000 }).should($itemList => {
cy.get('[data-cy=thumbnail-list]', { timeout: 50000 }).should($itemList => {
expect($itemList.length >= seriesToWait).to.be.true;
});
});
//Command to wait DICOM image to load into the viewport
Cypress.Commands.add('waitDicomImage', (timeout = 20000) => {
Cypress.Commands.add('waitDicomImage', (timeout = 50000) => {
const loaded = cy.isPageLoaded();
if (loaded) {
@@ -441,6 +442,12 @@ Cypress.Commands.add('openPreferences', () => {
});
});
Cypress.Commands.add('changePreferencesTab', tabAlias => {
cy.initPreferencesModalAliases();
cy.get(tabAlias).click();
initPreferencesModalFooterBtnAliases();
});
Cypress.Commands.add('resetUserHoktkeyPreferences', () => {
// Open User Preferences modal
cy.openPreferences();
@@ -457,7 +464,7 @@ Cypress.Commands.add(
(function_label, shortcut) => {
// Within scopes all `.get` and `.contains` to within the matched elements
// dom instead of checking from document
cy.get('.HotKeysPreferences')
cy.get('.HotkeysPreferences')
.within(() => {
cy.contains(function_label) // label we're looking for
.parent()
@@ -487,16 +494,16 @@ Cypress.Commands.add('setLanguage', (language, save = true) => {
.click()
.should('have.class', 'active');
initPreferencesModalFooterBtnAliases();
// Language dropdown should be displayed
cy.get('#language-select').should('be.visible');
// Select Language and Save/Cancel
cy.get('#language-select')
.select(language)
.then(() => {
const toClick = save ? '@saveBtn' : '@cancelBtn';
cy.get(toClick)
.scrollIntoView()
.click();
});
cy.get('#language-select').select(language);
const toClick = save ? '@saveBtn' : '@cancelBtn';
cy.get(toClick)
.scrollIntoView()
.click();
});
+7 -7
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/viewer",
"version": "3.6.0",
"version": "3.7.2",
"description": "OHIF Viewer",
"author": "OHIF Contributors",
"license": "MIT",
@@ -47,15 +47,15 @@
},
"dependencies": {
"@babel/runtime": "^7.5.5",
"@ohif/core": "^2.4.0",
"@ohif/extension-cornerstone": "^2.3.0",
"@ohif/core": "^2.5.1",
"@ohif/extension-cornerstone": "^2.4.0",
"@ohif/extension-dicom-html": "^1.1.0",
"@ohif/extension-dicom-microscopy": "^0.50.6",
"@ohif/extension-dicom-pdf": "^1.0.1",
"@ohif/extension-lesion-tracker": "^0.1.0",
"@ohif/extension-vtk": "^1.2.16",
"@ohif/i18n": "^0.52.5",
"@ohif/ui": "^1.1.9",
"@ohif/extension-lesion-tracker": "^0.2.0",
"@ohif/extension-vtk": "^1.2.21",
"@ohif/i18n": "^0.52.6",
"@ohif/ui": "^1.3.1",
"@tanem/react-nprogress": "^1.1.25",
"classnames": "^2.2.6",
"core-js": "^3.2.1",
+50 -4
View File
@@ -25,12 +25,12 @@ window.config = {
// ~ Global
{
commandName: 'incrementActiveViewport',
label: 'Next Image Viewport',
label: 'Next Viewport',
keys: ['right'],
},
{
commandName: 'decrementActiveViewport',
label: 'Previous Image Viewport',
label: 'Previous Viewport',
keys: ['left'],
},
// Supported Keys: https://craig.is/killing/mice
@@ -58,17 +58,63 @@ window.config = {
// firstImage
// lastImage
{
commandName: 'nextViewportDisplaySet',
commandName: 'previousViewportDisplaySet',
label: 'Previous Series',
keys: ['pagedown'],
},
{
commandName: 'previousViewportDisplaySet',
commandName: 'nextViewportDisplaySet',
label: 'Next Series',
keys: ['pageup'],
},
// ~ Cornerstone Tools
{ commandName: 'setZoomTool', label: 'Zoom', keys: ['z'] },
// ~ Window level presets
{
commandName: 'windowLevelPreset1',
label: 'W/L Preset 1',
keys: ['1'],
},
{
commandName: 'windowLevelPreset2',
label: 'W/L Preset 2',
keys: ['2'],
},
{
commandName: 'windowLevelPreset3',
label: 'W/L Preset 3',
keys: ['3'],
},
{
commandName: 'windowLevelPreset4',
label: 'W/L Preset 4',
keys: ['4'],
},
{
commandName: 'windowLevelPreset5',
label: 'W/L Preset 5',
keys: ['5'],
},
{
commandName: 'windowLevelPreset6',
label: 'W/L Preset 6',
keys: ['6'],
},
{
commandName: 'windowLevelPreset7',
label: 'W/L Preset 7',
keys: ['7'],
},
{
commandName: 'windowLevelPreset8',
label: 'W/L Preset 8',
keys: ['8'],
},
{
commandName: 'windowLevelPreset9',
label: 'W/L Preset 9',
keys: ['9'],
},
],
cornerstoneExtensionConfig: {},
};
+50 -4
View File
@@ -20,12 +20,12 @@ window.config = {
hotkeys: [
{
commandName: 'incrementActiveViewport',
label: 'Next Image Viewport',
label: 'Next Viewport',
keys: ['right'],
},
{
commandName: 'decrementActiveViewport',
label: 'Previous Image Viewport',
label: 'Previous Viewport',
keys: ['left'],
},
{ commandName: 'rotateViewportCW', label: 'Rotate Right', keys: ['r'] },
@@ -48,16 +48,62 @@ window.config = {
{ commandName: 'nextImage', label: 'Next Image', keys: ['down'] },
{ commandName: 'previousImage', label: 'Previous Image', keys: ['up'] },
{
commandName: 'nextViewportDisplaySet',
commandName: 'previousViewportDisplaySet',
label: 'Previous Series',
keys: ['pagedown'],
},
{
commandName: 'previousViewportDisplaySet',
commandName: 'nextViewportDisplaySet',
label: 'Next Series',
keys: ['pageup'],
},
{ commandName: 'setZoomTool', label: 'Zoom', keys: ['z'] },
// ~ Window level presets
{
commandName: 'windowLevelPreset1',
label: 'W/L Preset 1',
keys: ['1'],
},
{
commandName: 'windowLevelPreset2',
label: 'W/L Preset 2',
keys: ['2'],
},
{
commandName: 'windowLevelPreset3',
label: 'W/L Preset 3',
keys: ['3'],
},
{
commandName: 'windowLevelPreset4',
label: 'W/L Preset 4',
keys: ['4'],
},
{
commandName: 'windowLevelPreset5',
label: 'W/L Preset 5',
keys: ['5'],
},
{
commandName: 'windowLevelPreset6',
label: 'W/L Preset 6',
keys: ['6'],
},
{
commandName: 'windowLevelPreset7',
label: 'W/L Preset 7',
keys: ['7'],
},
{
commandName: 'windowLevelPreset8',
label: 'W/L Preset 8',
keys: ['8'],
},
{
commandName: 'windowLevelPreset9',
label: 'W/L Preset 9',
keys: ['9'],
},
],
i18n: {
LOCIZE_PROJECTID: 'a8da3f9a-e467-4dd6-af33-474d582a0294',
+50 -4
View File
@@ -20,12 +20,12 @@ window.config = {
// ~ Global
{
commandName: 'incrementActiveViewport',
label: 'Next Image Viewport',
label: 'Next Viewport',
keys: ['right'],
},
{
commandName: 'decrementActiveViewport',
label: 'Previous Image Viewport',
label: 'Previous Viewport',
keys: ['left'],
},
// Supported Keys: https://craig.is/killing/mice
@@ -53,17 +53,63 @@ window.config = {
// firstImage
// lastImage
{
commandName: 'nextViewportDisplaySet',
commandName: 'previousViewportDisplaySet',
label: 'Previous Series',
keys: ['pagedown'],
},
{
commandName: 'previousViewportDisplaySet',
commandName: 'nextViewportDisplaySet',
label: 'Next Series',
keys: ['pageup'],
},
// ~ Cornerstone Tools
{ commandName: 'setZoomTool', label: 'Zoom', keys: ['z'] },
// ~ Window level presets
{
commandName: 'windowLevelPreset1',
label: 'W/L Preset 1',
keys: ['1'],
},
{
commandName: 'windowLevelPreset2',
label: 'W/L Preset 2',
keys: ['2'],
},
{
commandName: 'windowLevelPreset3',
label: 'W/L Preset 3',
keys: ['3'],
},
{
commandName: 'windowLevelPreset4',
label: 'W/L Preset 4',
keys: ['4'],
},
{
commandName: 'windowLevelPreset5',
label: 'W/L Preset 5',
keys: ['5'],
},
{
commandName: 'windowLevelPreset6',
label: 'W/L Preset 6',
keys: ['6'],
},
{
commandName: 'windowLevelPreset7',
label: 'W/L Preset 7',
keys: ['7'],
},
{
commandName: 'windowLevelPreset8',
label: 'W/L Preset 8',
keys: ['8'],
},
{
commandName: 'windowLevelPreset9',
label: 'W/L Preset 9',
keys: ['9'],
},
],
i18n: {
LOCIZE_PROJECTID: 'a8da3f9a-e467-4dd6-af33-474d582a0294',
+21 -25
View File
@@ -53,7 +53,6 @@ import store from './store';
import WhiteLabellingContext from './context/WhiteLabellingContext';
import UserManagerContext from './context/UserManagerContext';
import AppContext from './context/AppContext';
const { setUserPreferences } = reduxOHIF.actions;
/** ~~~~~~~~~~~~~ Application Setup */
const commandsManagerConfig = {
@@ -63,8 +62,8 @@ const commandsManagerConfig = {
/** Managers */
const commandsManager = new CommandsManager(commandsManagerConfig);
const hotkeysManager = new HotkeysManager(commandsManager);
const servicesManager = new ServicesManager();
const hotkeysManager = new HotkeysManager(commandsManager, servicesManager);
let extensionManager;
/** ~~~~~~~~~~~~~ End Application Setup */
@@ -116,7 +115,7 @@ class App extends Component {
const {
servers,
hotkeys,
hotkeys: appConfigHotkeys,
cornerstoneExtensionConfig,
extensions,
oidc,
@@ -139,7 +138,7 @@ class App extends Component {
* Must run after extension commands are registered
* if there is no hotkeys from localStorage set up from config.
*/
_initHotkeys(hotkeys);
_initHotkeys(appConfigHotkeys);
_initServers(servers);
initWebWorkers();
}
@@ -263,30 +262,27 @@ function _initExtensions(extensions, cornerstoneExtensionConfig, appConfig) {
extensionManager.registerExtensions(mergedExtensions);
}
function _initHotkeys(hotkeys) {
const { hotkeyDefinitions = {} } = store.getState().preferences || {};
let updateStore = false;
let hotkeysToUse = hotkeyDefinitions;
/**
*
* @param {Object} appConfigHotkeys - Default hotkeys, as defined by app config
*/
function _initHotkeys(appConfigHotkeys) {
// TODO: Use something more resilient
// TODO: Mozilla has a special library for this
const userPreferredHotkeys = JSON.parse(
localStorage.getItem('hotkey-definitions') || '{}'
);
if (!Object.keys(hotkeyDefinitions).length) {
hotkeysToUse = hotkeys;
updateStore = true;
// TODO: hotkeysManager.isValidDefinitionObject(/* */)
const hasUserPreferences =
userPreferredHotkeys && Object.keys(userPreferredHotkeys).length > 0;
if (hasUserPreferences) {
hotkeysManager.setHotkeys(userPreferredHotkeys);
} else {
hotkeysManager.setHotkeys(appConfigHotkeys);
}
if (hotkeysToUse) {
hotkeysManager.setHotkeys(hotkeysToUse);
/* Set hotkeys default based on app config. */
hotkeysManager.setDefaultHotKeys(hotkeys);
if (updateStore) {
const { hotkeyDefinitions } = hotkeysManager;
const windowLevelData = {};
store.dispatch(
setUserPreferences({ windowLevelData, hotkeyDefinitions })
);
}
}
hotkeysManager.setDefaultHotKeys(appConfigHotkeys);
}
function _initServers(servers) {
@@ -1,39 +1,140 @@
import { redux } from "@ohif/core";
import store from "./../../store";
const { setViewportActive } = redux.actions;
import { redux } from '@ohif/core';
import store from './../../store';
const actions = {
updateViewportDisplaySet: ({ direction }) => {
// TODO
// console.warn('updateDisplaySet: ', direction);
},
updateActiveViewport: ({ viewports, direction }) => {
const { viewportSpecificData, activeViewportIndex } = viewports;
const maxIndex = Object.keys(viewportSpecificData).length - 1;
const commandsModule = ({ commandsManager }) => {
const { setViewportActive, setActiveViewportSpecificData } = redux.actions;
let newIndex = activeViewportIndex + direction;
newIndex = newIndex > maxIndex ? 0 : newIndex;
newIndex = newIndex < 0 ? maxIndex : newIndex;
const actions = {
updateActiveViewport: ({ viewports, direction }) => {
const { viewportSpecificData, activeViewportIndex } = viewports;
const maxIndex = Object.keys(viewportSpecificData).length - 1;
store.dispatch(setViewportActive(newIndex));
}
let newIndex = activeViewportIndex + direction;
newIndex = newIndex > maxIndex ? 0 : newIndex;
newIndex = newIndex < 0 ? maxIndex : newIndex;
store.dispatch(setViewportActive(newIndex));
},
setWindowLevelPreset: ({ viewports, preset }) => {
const state = store.getState();
const { preferences = {} } = state;
const { window, level } =
preferences.windowLevelData && preferences.windowLevelData[preset];
if (window && level) {
commandsManager.runCommand('setWindowLevel', {
viewports,
window,
level,
});
}
},
updateViewportDisplaySet: ({ viewports, direction }) => {
const viewportSpecificData = { ...viewports.viewportSpecificData };
const activeViewport =
viewportSpecificData[viewports.activeViewportIndex];
const studyMetadata = utils.studyMetadataManager.get(
activeViewport.studyInstanceUid
);
if (!studyMetadata) {
return;
}
const allDisplaySets = studyMetadata.getDisplaySets();
const currentDisplaySetIndex = allDisplaySets.findIndex(
displaySet =>
displaySet.displaySetInstanceUid ===
activeViewport.displaySetInstanceUid
);
if (currentDisplaySetIndex < 0) {
return;
}
const newDisplaySetIndex = currentDisplaySetIndex + direction;
const newDisplaySetData = allDisplaySets[newDisplaySetIndex];
if (!newDisplaySetData) {
return;
}
store.dispatch(setActiveViewportSpecificData(newDisplaySetData));
},
};
const definitions = {
// Next/Previous active viewport
incrementActiveViewport: {
commandFn: actions.updateActiveViewport,
storeContexts: ['viewports'],
options: { direction: 1 },
},
decrementActiveViewport: {
commandFn: actions.updateActiveViewport,
storeContexts: ['viewports'],
options: { direction: -1 },
},
// Window level Presets
windowLevelPreset1: {
commandFn: actions.setWindowLevelPreset,
storeContexts: ['viewports'],
options: { preset: 1 },
},
windowLevelPreset2: {
commandFn: actions.setWindowLevelPreset,
storeContexts: ['viewports'],
options: { preset: 2 },
},
windowLevelPreset3: {
commandFn: actions.setWindowLevelPreset,
storeContexts: ['viewports'],
options: { preset: 3 },
},
windowLevelPreset4: {
commandFn: actions.setWindowLevelPreset,
storeContexts: ['viewports'],
options: { preset: 4 },
},
windowLevelPreset5: {
commandFn: actions.setWindowLevelPreset,
storeContexts: ['viewports'],
options: { preset: 5 },
},
windowLevelPreset6: {
commandFn: actions.setWindowLevelPreset,
storeContexts: ['viewports'],
options: { preset: 6 },
},
windowLevelPreset7: {
commandFn: actions.setWindowLevelPreset,
storeContexts: ['viewports'],
options: { preset: 7 },
},
windowLevelPreset8: {
commandFn: actions.setWindowLevelPreset,
storeContexts: ['viewports'],
options: { preset: 8 },
},
windowLevelPreset9: {
commandFn: actions.setWindowLevelPreset,
storeContexts: ['viewports'],
options: { preset: 9 },
},
nextViewportDisplaySet: {
commandFn: actions.updateViewportDisplaySet,
storeContexts: ['viewports'],
options: { direction: 1 },
},
previousViewportDisplaySet: {
commandFn: actions.updateViewportDisplaySet,
storeContexts: ['viewports'],
options: { direction: -1 },
},
};
return {
definitions,
defaultContext: 'VIEWER',
};
};
const definitions = {
// Next/Previous active viewport
incrementActiveViewport: {
commandFn: actions.updateActiveViewport,
storeContexts: ["viewports"],
options: { direction: 1 }
},
decrementActiveViewport: {
commandFn: actions.updateActiveViewport,
storeContexts: ["viewports"],
options: { direction: -1 }
}
};
export default {
definitions,
defaultContext: "VIEWER"
};
export default commandsModule;
@@ -2,7 +2,7 @@ import commandsModule from './commandsModule.js';
export default {
id: 'generic-viewer-commands',
getCommandsModule() {
return commandsModule;
getCommandsModule({ commandsManager }) {
return commandsModule({ commandsManager });
},
};
@@ -1,10 +1,12 @@
import React, { useState, useEffect } from 'react';
import { Link, withRouter } from 'react-router-dom';
import { withTranslation } from 'react-i18next';
import PropTypes from 'prop-types';
import ConnectedUserPreferencesForm from '../../connectedComponents/ConnectedUserPreferencesForm';
import { Dropdown, AboutContent, withModal } from '@ohif/ui';
import { UserPreferences } from './../UserPreferences';
import OHIFLogo from '../OHIFLogo/OHIFLogo.js';
import './Header.css';
@@ -42,7 +44,7 @@ function Header(props) {
},
onClick: () =>
show({
content: ConnectedUserPreferencesForm,
content: UserPreferences,
title: t('User Preferences'),
}),
},
@@ -0,0 +1,72 @@
import React, { useState, useSelector } from 'react';
import PropTypes from 'prop-types';
import i18n from '@ohif/i18n';
import { TabFooter, LanguageSwitcher, useSnackbarContext } from '@ohif/ui';
import { useTranslation } from 'react-i18next';
import './GeneralPreferences.styl';
/**
* General Preferences tab
* It renders the General Preferences content
*
* @param {object} props component props
* @param {function} props.onClose
*/
function GeneralPreferences({ onClose }) {
const { t } = useTranslation('UserPreferencesModal');
const snackbar = useSnackbarContext();
const currentLanguage = i18n.language;
const { availableLanguages } = i18n;
const [language, setLanguage] = useState(currentLanguage);
const onResetPreferences = () => {
setLanguage(i18n.defaultLanguage);
};
const onSave = () => {
i18n.changeLanguage(language);
onClose();
snackbar.show({
message: t('SaveMessage'),
type: 'success',
});
};
const hasErrors = false;
return (
<React.Fragment>
<div className="GeneralPreferences">
<div className="language">
<label htmlFor="language-select" className="languageLabel">
Language
</label>
<LanguageSwitcher
language={language}
onLanguageChange={setLanguage}
languages={availableLanguages}
/>
</div>
</div>
<TabFooter
onResetPreferences={onResetPreferences}
onSave={onSave}
onCancel={onClose}
hasErrors={hasErrors}
t={t}
/>
</React.Fragment>
);
}
GeneralPreferences.propTypes = {
onClose: PropTypes.func,
};
export { GeneralPreferences };
@@ -0,0 +1,18 @@
.GeneralPreferences
display: flex;
padding: 20px 0;
.language
display: flex;
width: 50%;
.languageLabel
display: flex;
flex-basis: 0;
flex-grow: 1.5;
justify-content: flex-end;
padding-right: 20px;
.language-select
flex-grow: 1.5;
flex-basis: 0;
@@ -0,0 +1,203 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { useSnackbarContext, TabFooter, HotkeyField } from '@ohif/ui';
import { useTranslation } from 'react-i18next';
import { hotkeysValidators } from './hotkeysValidators';
import { MODIFIER_KEYS } from './hotkeysConfig';
import { hotkeysManager } from '../../App';
import './HotkeysPreferences.styl';
/**
* Take hotkeyDefenintions and build an initialState to be used into the component state
*
* @param {Object} hotkeyDefinitions
* @returns {Object} initialState
*/
const initialState = hotkeyDefinitions => ({
hotkeys: { ...hotkeyDefinitions },
errors: {},
});
/**
* Take the updated command and keys and validate the changes with all validators
*
* @param {Object} arguments
* @param {string} arguments.commandName command name string to be updated
* @param {array} arguments.pressedKeys new array of keys to be added for the commandName
* @param {array} arguments.hotkeys all hotkeys currently into the app
* @returns {Object} {errorMessage} errorMessage coming from any of the validator or undefined if none
*/
const validateCommandKey = ({ commandName, pressedKeys, hotkeys }) => {
for (const validator of hotkeysValidators) {
const validation = validator({
commandName,
pressedKeys,
hotkeys,
});
if (validation && validation.hasError) {
return validation;
}
}
return {
errorMessage: undefined,
};
};
/**
* Take all hotkeys and split the list into two lists
*
* @param {array} hotkeys list of all hotkeys
* @returns {array} array containing two arrays of keys
*/
const splitHotkeys = hotkeys => {
const splitedHotkeys = [];
const arrayHotkeys = Object.entries(hotkeys);
if (arrayHotkeys.length) {
const halfwayThrough = Math.ceil(arrayHotkeys.length / 2);
splitedHotkeys.push(arrayHotkeys.slice(0, halfwayThrough));
splitedHotkeys.push(
arrayHotkeys.slice(halfwayThrough, arrayHotkeys.length)
);
}
return splitedHotkeys;
};
/**
* HotkeysPreferences tab
* It renders all hotkeys displayed into columns/rows
*
* It stores current state and whenever it changes, component messages parent of new value (through function callback)
* @param {object} props component props
* @param {string} props.onClose
*/
function HotkeysPreferences({ onClose }) {
const { t } = useTranslation('UserPreferencesModal');
const { hotkeyDefaults, hotkeyDefinitions } = hotkeysManager;
const [state, setState] = useState(initialState(hotkeyDefinitions));
const snackbar = useSnackbarContext();
const onResetPreferences = () => {
const defaultHotKeyDefinitions = {};
hotkeyDefaults.map(item => {
const { commandName, ...values } = item;
defaultHotKeyDefinitions[commandName] = { ...values };
});
setState(initialState(defaultHotKeyDefinitions));
};
const onSave = () => {
const { hotkeys } = state;
hotkeysManager.setHotkeys(hotkeys);
localStorage.setItem('hotkey-definitions', JSON.stringify(hotkeys));
onClose();
snackbar.show({
message: t('SaveMessage'),
type: 'success',
});
};
const onHotkeyChanged = (commandName, hotkeyDefinition, keys) => {
const { errorMessage } = validateCommandKey({
commandName,
pressedKeys: keys,
hotkeys: state.hotkeys,
});
setState(prevState => ({
hotkeys: {
...prevState.hotkeys,
[commandName]: { ...hotkeyDefinition, keys },
},
errors: {
...prevState.errors,
[commandName]: errorMessage,
},
}));
};
const hasErrors = Object.keys(state.errors).some(key => !!state.errors[key]);
const hasHotkeys = Object.keys(state.hotkeys).length;
const splitedHotkeys = splitHotkeys(state.hotkeys);
return (
<React.Fragment>
<div className="HotkeysPreferences">
{hasHotkeys ? (
<div className="hotkeyTable">
{splitedHotkeys.map((hotkeys, index) => {
return (
<div className="hotkeyColumn" key={index}>
<div className="hotkeyHeader">
<div className="headerItemText text-right">Function</div>
<div className="headerItemText text-center">Shortcut</div>
</div>
{hotkeys.map(hotkey => {
const commandName = hotkey[0];
const hotkeyDefinition = hotkey[1];
const { keys, label } = hotkeyDefinition;
const errorMessage = state.errors[hotkey[0]];
const handleChange = keys => {
onHotkeyChanged(commandName, hotkeyDefinition, keys);
};
return (
<div key={commandName} className="hotkeyRow">
<div className="hotkeyLabel">{label}</div>
<div
data-key="defaultTool"
className={classnames(
'wrapperHotkeyInput',
errorMessage ? 'stateError' : ''
)}
>
<HotkeyField
keys={keys}
modifier_keys={MODIFIER_KEYS}
handleChange={handleChange}
classNames={'preferencesInput'}
></HotkeyField>
<span className="preferencesInputErrorMessage">
{errorMessage}
</span>
</div>
</div>
);
})}
</div>
);
})}
</div>
) : (
'Hotkeys definitions is empty'
)}
</div>
<TabFooter
onResetPreferences={onResetPreferences}
onSave={onSave}
onCancel={onClose}
hasErrors={hasErrors}
t={t}
/>
</React.Fragment>
);
}
HotkeysPreferences.propTypes = {
onClose: PropTypes.func,
};
export { HotkeysPreferences };
@@ -0,0 +1,46 @@
.HotkeysPreferences
display: flex
padding: 20px
.hotkeyTable
display: flex
flex-direction: row
flex-basis: 0
flex-grow: 1.5
.hotkeyColumn
display: flex
flex-direction: column
flex-basis: 0
flex-grow: 1.5
.hotkeyHeader
display: flex
flex-direction: row
margin-bottom: 10px
.headerItemText
flex-basis: 0
flex-grow: 1.5
padding: 5px 15px 5px 0
.hotkeyRow
display: flex
.wrapperHotkeyInput
margin-bottom: 5px
cursor: pointer
display: flex
flex-direction: column
flex-basis: 0
flex-grow: 1.5
.hotkeyLabel
padding: 5px 15px 5px 0
text-align: right
flex-basis: 0
flex-grow: 1.5
.stateError
.preferencesInput
background-color: var(--state-error)
@@ -0,0 +1,42 @@
import React from 'react';
import PropTypes from 'prop-types';
import { TabComponents } from '@ohif/ui';
// Tabs
import { HotkeysPreferences } from './HotkeysPreferences';
import { WindowLevelPreferences } from './WindowLevelPreferences';
import { GeneralPreferences } from './GeneralPreferences';
import './UserPreferences.styl';
const tabs = [
{
name: 'Hotkeys',
Component: HotkeysPreferences,
customProps: {},
},
{
name: 'General',
Component: GeneralPreferences,
customProps: {},
},
{
name: 'Window Level',
Component: WindowLevelPreferences,
customProps: {},
},
];
function UserPreferences({ hide }) {
const customProps = {
onClose: hide,
};
return <TabComponents tabs={tabs} customProps={customProps} />;
}
UserPreferences.propTypes = {
hide: PropTypes.func,
};
export { UserPreferences };
@@ -0,0 +1,26 @@
.preferencesInputErrorMessage
color: var(--state-error-text)
font-size: 10px
text-transform: uppercase
.preferencesInput
font-weight: 400
cursor: pointer
transition: background-color .3s ease,border-color .3s ease
background-color: var(--ui-gray)
color: var(--text-primary-color)
border-color: var(--ui-border-coolor)
border: 0
border-radius: 2px
font-size: 14px
height: 30px
width: 100%
line-height: 16px
padding: 8px 9px 6px
text-align: center
.preferencesInput:focus
border-color: var(--active-color)
background-color: var(--ui-gray-dark)
box-shadow: 0 0 0 2px var(--active-color) !important
outline: 0
@@ -0,0 +1,128 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { useSelector, useDispatch } from 'react-redux';
import { redux } from '@ohif/core';
import { TabFooter, useSnackbarContext } from '@ohif/ui';
import { useTranslation } from 'react-i18next';
const { actions } = redux;
import './WindowLevelPreferences.styl';
function WindowLevelPreferences({ onClose }) {
const dispatch = useDispatch();
const windowLevelData = useSelector(state => {
const { preferences = {} } = state;
const { windowLevelData } = preferences;
return windowLevelData;
});
const [state, setState] = useState({
values: { ...windowLevelData },
});
const { t } = useTranslation('UserPreferencesModal');
const onResetPreferences = () => {};
const hasErrors = false;
const onSave = () => {
dispatch(actions.setUserPreferences({ windowLevelData: state.values }));
onClose();
snackbar.show({
message: t('SaveMessage'),
type: 'success',
});
};
const snackbar = useSnackbarContext();
const handleInputChange = event => {
const $target = event.target;
const { key, inputname } = $target.dataset;
const inputValue = $target.value;
if (!state.values[key] || !state.values[key][inputname]) {
return;
}
setState(prevState => ({
...prevState,
values: {
...prevState.values,
[key]: {
...prevState.values[key],
[inputname]: inputValue,
},
},
}));
};
return (
<React.Fragment>
<div className="WindowLevelPreferences">
<div className="wlColumn">
<div className="wlRow header">
<div className="wlColumn preset">Preset</div>
<div className="wlColumn description">Description</div>
<div className="wlColumn window">Window</div>
<div className="wlColumn level">Level</div>
</div>
{Object.keys(state.values).map((key, index) => {
return (
<div className="wlRow" key={key}>
<div className="wlColumn preset">{key}</div>
<div className="wlColumn description">
<input
type="text"
className="preferencesInput"
value={state.values[key].description}
data-key={key}
data-inputname="description"
onChange={handleInputChange}
/>
</div>
<div className="wlColumn window">
<input
type="number"
className="preferencesInput"
value={state.values[key].window}
data-key={key}
data-inputname="window"
onChange={handleInputChange}
/>
</div>
<div className="wlColumn level">
<input
type="number"
className="preferencesInput"
value={state.values[key].level}
data-key={key}
data-inputname="level"
onChange={handleInputChange}
/>
</div>
</div>
);
})}
</div>
</div>
<TabFooter
onResetPreferences={onResetPreferences}
onSave={onSave}
onCancel={onClose}
hasErrors={hasErrors}
t={t}
/>
</React.Fragment>
);
}
WindowLevelPreferences.propTypes = {
onClose: PropTypes.func,
};
export { WindowLevelPreferences };
@@ -0,0 +1,28 @@
.WindowLevelPreferences
display: flex
padding: 20px 0
text-align: center
.wlColumn
display: flex
flex-direction: column
flex-basis: 0
flex-grow: 1.5
margin: 0 5px
.wlRow
display: flex
flex-direction: row
margin: 5px 0
.header
.preset
flex-grow: 0.5
.window,
.level
flex-grow: 1
.description
flex-grow: 2
@@ -2,7 +2,9 @@ const range = (start, end) => {
return new Array(end - start).fill().map((d, i) => i + start);
};
export const disallowedCombinations = {
export const MODIFIER_KEYS = ['ctrl', 'alt', 'shift'];
export const DISALLOWED_COMBINATIONS = {
'': [],
alt: ['space'],
shift: [],
@@ -34,18 +36,7 @@ export const disallowedCombinations = {
'ctrl+shift': ['q', 'w', 'r', 't', 'p', 'a', 'h', 'v', 'b', 'n'],
};
export const allowedKeys = [
...[8, 13, 27, 32, 46], // backspace, enter, escape, space, delete
...[12, 106, 107, 109, 110, 111], // Numpad keys
...range(218, 220), // [\]
...range(185, 190), // ;=,-./
...range(111, 131), // F1-F19
...range(32, 41), // arrow keys, home/end, pg dn/up
...range(47, 58), // 0-9
...range(64, 91), // A-Z
];
export const specialKeys = {
export const SPECIAL_KEYS = {
8: 'backspace',
9: 'tab',
13: 'return',
@@ -0,0 +1,97 @@
import { MODIFIER_KEYS, DISALLOWED_COMBINATIONS } from './hotkeysConfig';
const formatPressedKeys = pressedKeysArray => pressedKeysArray.join('+');
const findConflictingCommand = (hotkeys, currentCommandName, pressedKeys) => {
let firstConflictingCommand = undefined;
const formatedPressedHotkeys = formatPressedKeys(pressedKeys);
for (const commandName in hotkeys) {
const toolHotkeys = hotkeys[commandName].keys;
const formatedToolHotkeys = formatPressedKeys(toolHotkeys);
if (
formatedPressedHotkeys === formatedToolHotkeys &&
commandName !== currentCommandName
) {
firstConflictingCommand = hotkeys[commandName];
break;
}
}
return firstConflictingCommand;
};
const ERROR_MESSAGES = {
MODIFIER:
"It's not possible to define only modifier keys (ctrl, alt and shift) as a shortcut",
EMPTY: "Field can't be empty.",
};
// VALIDATORS
const modifierValidator = ({ pressedKeys }) => {
const lastPressedKey = pressedKeys[pressedKeys.length - 1];
// Check if it has a valid modifier
const isModifier = MODIFIER_KEYS.includes(lastPressedKey);
if (isModifier) {
return {
hasError: true,
errorMessage: ERROR_MESSAGES.MODIFIER,
};
}
};
const emptyValidator = ({ pressedKeys = [] }) => {
if (!pressedKeys.length) {
return {
hasError: true,
errorMessage: ERROR_MESSAGES.EMPTY,
};
}
};
const conflictingValidator = ({ commandName, pressedKeys, hotkeys }) => {
const conflictingCommand = findConflictingCommand(
hotkeys,
commandName,
pressedKeys
);
if (conflictingCommand) {
return {
hasError: true,
errorMessage: `"${conflictingCommand.label}" is already using the "${pressedKeys}" shortcut.`,
};
}
};
const disallowedValidator = ({ pressedKeys = [] }) => {
const lastPressedKey = pressedKeys[pressedKeys.length - 1];
const modifierCommand = formatPressedKeys(
pressedKeys.slice(0, pressedKeys.length - 1)
);
const disallowedCombination = DISALLOWED_COMBINATIONS[modifierCommand];
const hasDisallowedCombinations = disallowedCombination
? disallowedCombination.includes(lastPressedKey)
: false;
if (hasDisallowedCombinations) {
return {
hasError: true,
errorMessage: `"${formatPressedKeys(
pressedKeys
)}" shortcut combination is not allowed`,
};
}
};
const hotkeysValidators = [
emptyValidator,
modifierValidator,
conflictingValidator,
disallowedValidator,
];
export { hotkeysValidators };
@@ -0,0 +1 @@
export { UserPreferences } from './UserPreferences';
Loaded 100 of 102 files, more files were not shown because too many files have changed in this diff. Show more