fixes for hotkeys usage in docusaurus
This commit is contained in:
parent
7a9f113b91
commit
ff28a2264b
@ -2,7 +2,7 @@
|
||||
<!-- markdownlint-disable -->
|
||||
<div align="center">
|
||||
<h1>OHIF Medical Imaging Viewer</h1>
|
||||
<p><strong>The OHIF Viewer</strong> is a zero-footprint medical image viewer provided by the <a href="http://ohif.org/">Open Health Imaging Foundation (OHIF)</a>. It is a configurable and extensible progressive web application with out-of-the-box support for image archives which support <a href="https://www.dicomstandard.org/dicomweb/">DICOMweb</a>.</p>
|
||||
<p><strong>The OHIF Viewer</strong> is a zero-footprint medical image viewer provided by the <a href="https://ohif.org/">Open Health Imaging Foundation (OHIF)</a>. It is a configurable and extensible progressive web application with out-of-the-box support for image archives which support <a href="https://www.dicomstandard.org/dicomweb/">DICOMweb</a>.</p>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
@ -11,11 +11,9 @@ import {
|
||||
} from '@ohif/ui';
|
||||
|
||||
import i18n from '@ohif/i18n';
|
||||
import { utils } from '@ohif/ui';
|
||||
import { hotkeys } from '@ohif/core';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
const { hotkeys } = utils;
|
||||
|
||||
const { availableLanguages, defaultLanguage, currentLanguage } = i18n;
|
||||
|
||||
import { useAppConfig } from '@state';
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import toolbarButtons from './toolbarButtons.js';
|
||||
import { utils } from '@ohif/ui';
|
||||
const { hotkeys } = utils;
|
||||
import { hotkeys } from '@ohif/core';
|
||||
|
||||
const ohif = {
|
||||
layout: 'org.ohif.default.layoutTemplateModule.viewerLayout',
|
||||
|
||||
@ -34,7 +34,8 @@
|
||||
"cornerstone-tools": "5.1.2",
|
||||
"cornerstone-math": "0.1.9",
|
||||
"cornerstone-wado-image-loader": "^3.1.2",
|
||||
"dicom-parser": "^1.8.3"
|
||||
"dicom-parser": "^1.8.3",
|
||||
"@ohif/ui": "^1.8.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "7.7.6",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import objectHash from 'object-hash';
|
||||
import log from './../log.js';
|
||||
import hotkeys from './../utils/hotkeys';
|
||||
import { hotkeys } from '../utils';
|
||||
|
||||
/**
|
||||
*
|
||||
|
||||
@ -46,7 +46,7 @@ const utils = {
|
||||
resolveObjectPath,
|
||||
hierarchicalListUtils,
|
||||
progressTrackingUtils,
|
||||
isLowPriorityModality
|
||||
isLowPriorityModality,
|
||||
};
|
||||
|
||||
export {
|
||||
@ -69,7 +69,7 @@ export {
|
||||
resolveObjectPath,
|
||||
hierarchicalListUtils,
|
||||
progressTrackingUtils,
|
||||
isLowPriorityModality
|
||||
isLowPriorityModality,
|
||||
};
|
||||
|
||||
export default utils;
|
||||
|
||||
@ -70,7 +70,7 @@ and registered extension's features, are configured using this file.
|
||||
|
||||
The easiest way to apply your own configuration is to modify the `default.js`
|
||||
file. For more advanced cofiguration options, check out our
|
||||
[configuration essentials guide](../configuring/index.md).
|
||||
[configuration essentials guide](../../configuration/index.md).
|
||||
|
||||
## Next Steps
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
---
|
||||
sidebar_position: 2
|
||||
sidebar_position: 4
|
||||
sidebar_label: Issue & PR Triage Process
|
||||
---
|
||||
|
||||
|
||||
@ -2,27 +2,17 @@
|
||||
sidebar_position: 3
|
||||
sidebar_label: Data Source
|
||||
---
|
||||
|
||||
# Module: Data Source
|
||||
|
||||
|
||||
## Overview
|
||||
The internal data structure of OHIF’s metadata follows naturalized DICOM JSON, A format pioneered by `dcmjs`. In short DICOM metadata headers with DICOM Keywords instead of tags and sequences as arrays, for easy development and clear code.
|
||||
|
||||
The internal data structure of OHIF’s metadata follows naturalized DICOM JSON, A
|
||||
format pioneered by `dcmjs`. In short DICOM metadata headers with DICOM Keywords
|
||||
instead of tags and sequences as arrays, for easy development and clear code.
|
||||
We have built a standard for fetching and mapping data into OHIF’s native format, which we call DataSources, and have provided one implementation of this standard.
|
||||
|
||||
We have built a standard for fetching and mapping data into OHIF’s native
|
||||
format, which we call DataSources, and have provided one implementation of this
|
||||
standard.
|
||||
You can make another datasource implementation which communicates to your backend and maps to OHIF’s native format, then use any existing mode on your platform. Your data doesn’t even need to be DICOM if you can map some proprietary data to the correct format.
|
||||
|
||||
You can make another datasource implementation which communicates to your
|
||||
backend and maps to OHIF’s native format, then use any existing mode on your
|
||||
platform. Your data doesn’t even need to be DICOM if you can map some
|
||||
proprietary data to the correct format.
|
||||
|
||||
The DataSource is also a place to add easy helper methods that platform-specific
|
||||
extensions can call in order to interact with the backend, meaning proprietary
|
||||
data interactions can be wrapped in extensions.
|
||||
The DataSource is also a place to add easy helper methods that platform-specific extensions can call in order to interact with the backend, meaning proprietary data interactions can be wrapped in extensions.
|
||||
|
||||
```js
|
||||
const getDataSourcesModule = () => [
|
||||
@ -36,13 +26,13 @@ const getDataSourcesModule = () => [
|
||||
];
|
||||
```
|
||||
|
||||
Default extension provides two main data sources that are commonly used:
|
||||
`dicomweb` and `dicomjson`
|
||||
Default extension provides two main data sources that are commonly used: `dicomweb` and `dicomjson`
|
||||
|
||||
```js
|
||||
import { createDicomWebApi } from './DicomWebDataSource/index.js';
|
||||
import { createDicomJSONApi } from './DicomJSONDataSource/index.js';
|
||||
|
||||
|
||||
function getDataSourcesModule() {
|
||||
return [
|
||||
{
|
||||
@ -59,14 +49,13 @@ function getDataSourcesModule() {
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## Custom DataSource
|
||||
You can add your custom datasource by creating the implementation using `IWebApiDataSource.create` from `@ohif/core`. This factory function creates a new "Web API" data source that fetches data over HTTP.
|
||||
|
||||
You need to make sure, you implement the following functions for the data source.
|
||||
|
||||
You can add your custom datasource by creating the implementation using
|
||||
`IWebApiDataSource.create` from `@ohif/core`. This factory function creates a
|
||||
new "Web API" data source that fetches data over HTTP.
|
||||
|
||||
You need to make sure, you implement the following functions for the data
|
||||
source.
|
||||
|
||||
```js title="platform/core/src/DataSources/IWebApiDataSource.js"
|
||||
function create({
|
||||
@ -87,9 +76,8 @@ function create({
|
||||
You can take a look at `dicomweb` data source implementation to get an idea
|
||||
`extensions/default/src/DicomWebDataSource/index.js`
|
||||
|
||||
## DicomMetadataStore
|
||||
|
||||
In `OHIF-v3` we have a central location for the metadata of studies and they are
|
||||
located in `DicomMetadataStore`. Your custom datasource can communicate with
|
||||
`DicomMetadataStore` to store, and fetch Study/Series/Instance metadata. We will
|
||||
learn more about `DicomMetadataStore` in services.
|
||||
|
||||
## DicomMetadataStore
|
||||
In `OHIF-v3` we have a central location for the metadata of studies and they are located
|
||||
in `DicomMetadataStore`. Your custom datasource can communicate with `DicomMetadataStore` to store, and fetch Study/Series/Instance metadata. We will learn more about `DicomMetadataStore` in services.
|
||||
|
||||
@ -78,7 +78,7 @@ There are two types of `routes` that are created by the mode.
|
||||
- Routes with dataSourceName `/${mode.id}/${dataSourceName}`
|
||||
- Routes without dataSourceName `/${mode.id}`
|
||||
|
||||
Therefore navigating to `http://localhost:3000/viewer/?StudyInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125113417.1` will run the app with the layout and functionalities of the `viewer` mode using the `defaultDataSourceName` which is defined in the [App Config](../configuring/index.md)
|
||||
Therefore navigating to `http://localhost:3000/viewer/?StudyInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125113417.1` will run the app with the layout and functionalities of the `viewer` mode using the `defaultDataSourceName` which is defined in the [App Config](../../configuration/index.md)
|
||||
|
||||
You can use the same exact mode using a different registered data source (e.g., `dicomjson`) by navigating to `http://localhost:3000/viewer/dicomjson/?StudyInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125113417.1`
|
||||
|
||||
|
||||
@ -72,7 +72,7 @@ const AboutModal = ({buildNumber, versionNumber}) => {
|
||||
</Link>
|
||||
</span>
|
||||
<span className="ml-4">
|
||||
<Link href="http://ohif.org/" showIcon={true}>
|
||||
<Link href="https://ohif.org/" showIcon={true}>
|
||||
More details
|
||||
</Link>
|
||||
</span>
|
||||
|
||||
@ -2,7 +2,6 @@ import React, { useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import Input from '../Input';
|
||||
import { hotkeys } from '../../utils/'
|
||||
import { getKeys, formatKeysForInput } from './utils';
|
||||
|
||||
/**
|
||||
@ -16,7 +15,7 @@ import { getKeys, formatKeysForInput } from './utils';
|
||||
* @param {string} props.className input classes
|
||||
* @param {Array[]} props.modifierKeys
|
||||
*/
|
||||
const HotkeyField = ({ disabled, keys, onChange, className, modifierKeys }) => {
|
||||
const HotkeyField = ({ disabled, keys, onChange, className, modifierKeys, hotkeys }) => {
|
||||
const inputValue = formatKeysForInput(keys);
|
||||
|
||||
useEffect(() => {
|
||||
@ -57,6 +56,13 @@ HotkeyField.propTypes = {
|
||||
className: PropTypes.string,
|
||||
modifierKeys: PropTypes.array,
|
||||
disabled: PropTypes.bool,
|
||||
hotkeys: PropTypes.object({
|
||||
initialize: PropTypes.func.isRequired,
|
||||
pause: PropTypes.func.isRequired,
|
||||
unpause: PropTypes.func.isRequired,
|
||||
startRecording: PropTypes.func.isRequired,
|
||||
record: PropTypes.func.isRequired,
|
||||
}).isRequired
|
||||
};
|
||||
|
||||
HotkeyField.defaultProps = {
|
||||
|
||||
@ -9,7 +9,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import { MODIFIER_KEYS } from './hotkeysConfig';
|
||||
import { validate, splitHotkeyDefinitionsAndCreateTuples } from './utils';
|
||||
|
||||
const HotkeysPreferences = ({ disabled, hotkeyDefinitions, errors: controlledErrors, onChange }) => {
|
||||
const HotkeysPreferences = ({ disabled, hotkeyDefinitions, errors: controlledErrors, onChange, hotkeysModule }) => {
|
||||
const { t } = useTranslation('UserPreferencesModal');
|
||||
|
||||
const visibleHotkeys = Object.keys(hotkeyDefinitions)
|
||||
@ -83,6 +83,7 @@ const HotkeysPreferences = ({ disabled, hotkeyDefinitions, errors: controlledErr
|
||||
keys={definition.keys}
|
||||
modifierKeys={MODIFIER_KEYS}
|
||||
onChange={onChangeHandler}
|
||||
hotkeys={hotkeysModule}
|
||||
className='text-lg h-8'
|
||||
/>
|
||||
{error && <span className='p-2 text-left text-red-600 text-sm'>{error}</span>}
|
||||
@ -106,6 +107,13 @@ HotkeysPreferences.propTypes = {
|
||||
onChange: PropTypes.func,
|
||||
disabled: PropTypes.bool,
|
||||
hotkeyDefinitions: PropTypes.object.isRequired,
|
||||
hotkeysModule: PropTypes.object({
|
||||
initialize: PropTypes.func.isRequired,
|
||||
pause: PropTypes.func.isRequired,
|
||||
unpause: PropTypes.func.isRequired,
|
||||
startRecording: PropTypes.func.isRequired,
|
||||
record: PropTypes.func.isRequired,
|
||||
}).isRequired
|
||||
};
|
||||
|
||||
HotkeysPreferences.defaultProps = {
|
||||
|
||||
@ -46,7 +46,4 @@ const validate = ({ commandName, pressedKeys, hotkeys }) => {
|
||||
return { error: undefined };
|
||||
};
|
||||
|
||||
export {
|
||||
validate,
|
||||
splitHotkeyDefinitionsAndCreateTuples
|
||||
};
|
||||
export { validate, splitHotkeyDefinitionsAndCreateTuples };
|
||||
|
||||
@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
|
||||
import { Select, Typography, Button, HotkeysPreferences } from '../';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const UserPreferences = ({ availableLanguages, defaultLanguage, currentLanguage, disabled, hotkeyDefinitions, hotkeyDefaults, onCancel, onSubmit, onReset }) => {
|
||||
const UserPreferences = ({ availableLanguages, defaultLanguage, currentLanguage, disabled, hotkeyDefinitions, hotkeyDefaults, onCancel, onSubmit, onReset, hotkeysModule }) => {
|
||||
const { t } = useTranslation('UserPreferencesModal');
|
||||
const [state, setState] = useState({
|
||||
isDisabled: disabled,
|
||||
@ -85,6 +85,7 @@ const UserPreferences = ({ availableLanguages, defaultLanguage, currentLanguage,
|
||||
hotkeyDefinitions={state.hotkeyDefinitions}
|
||||
onChange={onHotkeysChangeHandler}
|
||||
errors={state.hotkeyErrors}
|
||||
hotkeysModule={hotkeysModule}
|
||||
/>
|
||||
</Section>
|
||||
<div className="flex flex-row justify-between">
|
||||
@ -125,6 +126,13 @@ UserPreferences.propTypes = {
|
||||
onCancel: PropTypes.func,
|
||||
onSubmit: PropTypes.func,
|
||||
onReset: PropTypes.func,
|
||||
hotkeysModule: PropTypes.object({
|
||||
initialize: PropTypes.func.isRequired,
|
||||
pause: PropTypes.func.isRequired,
|
||||
unpause: PropTypes.func.isRequired,
|
||||
startRecording: PropTypes.func.isRequired,
|
||||
record: PropTypes.func.isRequired,
|
||||
}).isRequired
|
||||
};
|
||||
|
||||
UserPreferences.defaultProps = {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
/** UTILS */
|
||||
import utils from './utils';
|
||||
export { utils };
|
||||
//import utils from './utils';
|
||||
//export { utils };
|
||||
|
||||
/** CONTEXT/HOOKS */
|
||||
export {
|
||||
@ -102,6 +102,3 @@ export {
|
||||
export { getIcon, ICONS } from './components/Icon/getIcon';
|
||||
export { BackgroundColor } from './pages/Colors/BackgroundColor';
|
||||
export { ModalComponent } from './contextProviders/ModalComponent';
|
||||
|
||||
/** VIEWS */
|
||||
export { StudyList, Viewer } from './views';
|
||||
|
||||
@ -1,17 +0,0 @@
|
||||
import studyListMock from '../mocks/studyList.json';
|
||||
|
||||
/** Values can be env vars */
|
||||
const DEFAULT_MOCKED_STUDIES_LIMIT = 1000;
|
||||
|
||||
/**
|
||||
* Method to get a mocked study list
|
||||
* @param {number} items Number of studies to be loaded
|
||||
* @returns {array} Study list
|
||||
*/
|
||||
const getMockedStudies = (items = 50) => {
|
||||
const num =
|
||||
items > DEFAULT_MOCKED_STUDIES_LIMIT ? DEFAULT_MOCKED_STUDIES_LIMIT : items;
|
||||
return new Array(num).fill(studyListMock.studies[0]);
|
||||
};
|
||||
|
||||
export default getMockedStudies;
|
||||
@ -1,14 +0,0 @@
|
||||
import Mousetrap from 'mousetrap';
|
||||
import pausePlugin from './pausePlugin';
|
||||
import recordPlugin from './recordPlugin';
|
||||
|
||||
Mousetrap.initialize = () => {
|
||||
if (!Mousetrap._initialized) {
|
||||
recordPlugin(Mousetrap);
|
||||
pausePlugin(Mousetrap);
|
||||
|
||||
Mousetrap._initialized = true;
|
||||
}
|
||||
};
|
||||
|
||||
export default Mousetrap;
|
||||
@ -1,32 +0,0 @@
|
||||
/**
|
||||
* adds a pause and unpause method to Mousetrap
|
||||
* this allows you to enable or disable keyboard shortcuts
|
||||
* without having to reset Mousetrap and rebind everything
|
||||
*
|
||||
* https://github.com/ccampbell/mousetrap/blob/master/plugins/pause/mousetrap-pause.js
|
||||
*/
|
||||
export default function(Mousetrap) {
|
||||
var _originalStopCallback = Mousetrap.prototype.stopCallback;
|
||||
|
||||
Mousetrap.prototype.stopCallback = function(e, element, combo) {
|
||||
var self = this;
|
||||
|
||||
if (self.paused) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return _originalStopCallback.call(self, e, element, combo);
|
||||
};
|
||||
|
||||
Mousetrap.prototype.pause = function() {
|
||||
var self = this;
|
||||
self.paused = true;
|
||||
};
|
||||
|
||||
Mousetrap.prototype.unpause = function() {
|
||||
var self = this;
|
||||
self.paused = false;
|
||||
};
|
||||
|
||||
Mousetrap.init();
|
||||
}
|
||||
@ -1,218 +0,0 @@
|
||||
/**
|
||||
* This extension allows you to record a sequence using Mousetrap.
|
||||
* {@link https://craig.is/killing/mice}
|
||||
*
|
||||
* @author Dan Tao <daniel.tao@gmail.com>
|
||||
*/
|
||||
export default function (Mousetrap) {
|
||||
/**
|
||||
* the sequence currently being recorded
|
||||
*
|
||||
* @type {Array}
|
||||
*/
|
||||
var _recordedSequence = [],
|
||||
/**
|
||||
* a callback to invoke after recording a sequence
|
||||
*
|
||||
* @type {Function|null}
|
||||
*/
|
||||
_recordedSequenceCallback = null,
|
||||
/**
|
||||
* a list of all of the keys currently held down
|
||||
*
|
||||
* @type {Array}
|
||||
*/
|
||||
_currentRecordedKeys = [],
|
||||
/**
|
||||
* temporary state where we remember if we've already captured a
|
||||
* character key in the current combo
|
||||
*
|
||||
* @type {boolean}
|
||||
*/
|
||||
_recordedCharacterKey = false,
|
||||
/**
|
||||
* a handle for the timer of the current recording
|
||||
*
|
||||
* @type {null|number}
|
||||
*/
|
||||
_recordTimer = null,
|
||||
/**
|
||||
* the original handleKey method to override when Mousetrap.record() is
|
||||
* called
|
||||
*
|
||||
* @type {Function}
|
||||
*/
|
||||
_origHandleKey = Mousetrap.prototype.handleKey;
|
||||
|
||||
/**
|
||||
* handles a character key event
|
||||
*
|
||||
* @param {string} character
|
||||
* @param {Array} modifiers
|
||||
* @param {Event} e
|
||||
* @returns void
|
||||
*/
|
||||
function _handleKey(character, modifiers, e) {
|
||||
var self = this;
|
||||
|
||||
if (!self.recording) {
|
||||
_origHandleKey.apply(self, arguments);
|
||||
return;
|
||||
}
|
||||
|
||||
// remember this character if we're currently recording a sequence
|
||||
if (e.type == 'keydown') {
|
||||
if (character.length === 1 && _recordedCharacterKey) {
|
||||
_recordCurrentCombo();
|
||||
}
|
||||
|
||||
for (let i = 0; i < modifiers.length; ++i) {
|
||||
_recordKey(modifiers[i]);
|
||||
}
|
||||
_recordKey(character);
|
||||
|
||||
// once a key is released, all keys that were held down at the time
|
||||
// count as a keypress
|
||||
} else if (e.type == 'keyup' && _currentRecordedKeys.length > 0) {
|
||||
_recordCurrentCombo();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* marks a character key as held down while recording a sequence
|
||||
*
|
||||
* @param {string} key
|
||||
* @returns void
|
||||
*/
|
||||
function _recordKey(key) {
|
||||
// one-off implementation of Array.indexOf, since IE6-9 don't support it
|
||||
for (let i = 0; i < _currentRecordedKeys.length; ++i) {
|
||||
if (_currentRecordedKeys[i] === key) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
_currentRecordedKeys.push(key);
|
||||
|
||||
if (key.length === 1) {
|
||||
_recordedCharacterKey = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* marks whatever key combination that's been recorded so far as finished
|
||||
* and gets ready for the next combo
|
||||
*
|
||||
* @returns void
|
||||
*/
|
||||
function _recordCurrentCombo() {
|
||||
_recordedSequence.push(_currentRecordedKeys);
|
||||
_currentRecordedKeys = [];
|
||||
_recordedCharacterKey = false;
|
||||
_finishRecording();
|
||||
}
|
||||
|
||||
/**
|
||||
* ensures each combo in a sequence is in a predictable order and formats
|
||||
* key combos to be '+'-delimited
|
||||
*
|
||||
* modifies the sequence in-place
|
||||
*
|
||||
* @param {Array} sequence
|
||||
* @returns void
|
||||
*/
|
||||
function _normalizeSequence(sequence) {
|
||||
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) {
|
||||
return -1;
|
||||
} else if (x.length === 1 && y.length > 1) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// character keys come next (list should contain no duplicates,
|
||||
// so no need for equality check)
|
||||
return x > y ? 1 : -1;
|
||||
});
|
||||
|
||||
sequence[i] = sequence[i].join('+');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* finishes the current recording, passes the recorded sequence to the stored
|
||||
* callback, and sets Mousetrap.handleKey back to its original function
|
||||
*
|
||||
* @returns void
|
||||
*/
|
||||
function _finishRecording() {
|
||||
if (_recordedSequenceCallback) {
|
||||
_normalizeSequence(_recordedSequence);
|
||||
_recordedSequenceCallback(_recordedSequence);
|
||||
}
|
||||
|
||||
// reset all recorded state
|
||||
_recordedSequence = [];
|
||||
_recordedSequenceCallback = null;
|
||||
_currentRecordedKeys = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* called to set a 1 second timeout on the current recording
|
||||
*
|
||||
* this is so after each key press in the sequence the recording will wait for
|
||||
* 1 more second before executing the callback
|
||||
*
|
||||
* @returns void
|
||||
*/
|
||||
function _restartRecordTimer() {
|
||||
clearTimeout(_recordTimer);
|
||||
_recordTimer = setTimeout(_finishRecording, 1000);
|
||||
}
|
||||
|
||||
/**
|
||||
* records the next sequence and passes it to a callback once it's
|
||||
* completed
|
||||
*
|
||||
* @param {Function} callback
|
||||
* @returns void
|
||||
*/
|
||||
Mousetrap.prototype.record = function (callback) {
|
||||
var self = this;
|
||||
self.recording = true;
|
||||
_recordedSequenceCallback = function () {
|
||||
self.recording = false;
|
||||
callback.apply(self, arguments);
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 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);
|
||||
};
|
||||
|
||||
Mousetrap.init();
|
||||
}
|
||||
@ -1,8 +0,0 @@
|
||||
import getMockedStudies from './getMockedStudies';
|
||||
import hotkeys from './hotkeys';
|
||||
|
||||
const utils = { getMockedStudies, hotkeys };
|
||||
|
||||
export { getMockedStudies, hotkeys };
|
||||
|
||||
export default utils;
|
||||
@ -1,295 +0,0 @@
|
||||
/**
|
||||
* THIS IS A TEMPORARY FILE -- SHOULD BE REMOVED
|
||||
*/
|
||||
import React, { useState } from 'react';
|
||||
import classnames from 'classnames';
|
||||
import moment from 'moment';
|
||||
|
||||
import {
|
||||
EmptyStudies,
|
||||
Icon,
|
||||
StudyListExpandedRow,
|
||||
Button,
|
||||
StudyListPagination,
|
||||
StudyListTable,
|
||||
StudyListFilter,
|
||||
} from '../../components';
|
||||
import utils from '../../utils';
|
||||
|
||||
// fix imports after refactor
|
||||
import Header from './components/Header';
|
||||
|
||||
const filtersMeta = [
|
||||
{
|
||||
name: 'patientName',
|
||||
displayName: 'Patient Name',
|
||||
inputType: 'Text',
|
||||
isSortable: true,
|
||||
gridCol: 4,
|
||||
},
|
||||
{
|
||||
name: 'mrn',
|
||||
displayName: 'MRN',
|
||||
inputType: 'Text',
|
||||
isSortable: true,
|
||||
gridCol: 2,
|
||||
},
|
||||
{
|
||||
name: 'studyDate',
|
||||
displayName: 'Study date',
|
||||
inputType: 'DateRange',
|
||||
isSortable: true,
|
||||
gridCol: 5,
|
||||
},
|
||||
{
|
||||
name: 'description',
|
||||
displayName: 'Description',
|
||||
inputType: 'Text',
|
||||
isSortable: true,
|
||||
gridCol: 4,
|
||||
},
|
||||
{
|
||||
name: 'modality',
|
||||
displayName: 'Modality',
|
||||
inputType: 'MultiSelect',
|
||||
inputProps: {
|
||||
options: [
|
||||
{ value: 'SEG', label: 'SEG' },
|
||||
{ value: 'CT', label: 'CT' },
|
||||
{ value: 'MR', label: 'MR' },
|
||||
{ value: 'SR', label: 'SR' },
|
||||
],
|
||||
},
|
||||
isSortable: true,
|
||||
gridCol: 3,
|
||||
},
|
||||
{
|
||||
name: 'accession',
|
||||
displayName: 'Accession',
|
||||
inputType: 'Text',
|
||||
isSortable: true,
|
||||
gridCol: 4,
|
||||
},
|
||||
{
|
||||
name: 'instances',
|
||||
displayName: 'Instances',
|
||||
inputType: 'None',
|
||||
isSortable: true,
|
||||
gridCol: 2,
|
||||
},
|
||||
];
|
||||
|
||||
const defaultFilterValues = {
|
||||
patientName: '',
|
||||
mrn: '',
|
||||
studyDate: {
|
||||
startDate: null,
|
||||
endDate: null,
|
||||
},
|
||||
description: '',
|
||||
modality: undefined,
|
||||
accession: '',
|
||||
sortBy: '',
|
||||
sortDirection: 'none',
|
||||
page: 0,
|
||||
resultsPerPage: 25,
|
||||
};
|
||||
|
||||
const isFiltering = (filterValues, defaultFilterValues) => {
|
||||
return Object.keys(defaultFilterValues).some(name => {
|
||||
return filterValues[name] !== defaultFilterValues[name];
|
||||
});
|
||||
};
|
||||
|
||||
const StudyList = () => {
|
||||
const [filterValues, setFilterValues] = useState(defaultFilterValues);
|
||||
const studies = utils.getMockedStudies();
|
||||
const numOfStudies = studies.length;
|
||||
const [expandedRows, setExpandedRows] = useState([]);
|
||||
|
||||
const tableDataSource = studies.map((study, key) => {
|
||||
const rowKey = key + 1;
|
||||
const isExpanded = expandedRows.some(k => k === rowKey);
|
||||
const {
|
||||
AccessionNumber,
|
||||
Modalities,
|
||||
Instances,
|
||||
StudyDescription,
|
||||
PatientId,
|
||||
PatientName,
|
||||
StudyDate,
|
||||
series,
|
||||
} = study;
|
||||
|
||||
const seriesTableColumns = {
|
||||
description: 'Description',
|
||||
seriesNumber: 'Series',
|
||||
modality: 'Modality',
|
||||
Instances: 'Instances',
|
||||
};
|
||||
|
||||
const seriesTableDataSource = series.map(seriesItem => {
|
||||
const { SeriesNumber, Modality, instances } = seriesItem;
|
||||
return {
|
||||
description: 'Patient Protocol',
|
||||
seriesNumber: SeriesNumber,
|
||||
modality: Modality,
|
||||
Instances: instances.length,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
row: [
|
||||
{
|
||||
key: 'patientName',
|
||||
content: PatientName,
|
||||
gridCol: 4,
|
||||
},
|
||||
{
|
||||
key: 'mrn',
|
||||
content: PatientId,
|
||||
gridCol: 2,
|
||||
},
|
||||
{
|
||||
key: 'studyDate',
|
||||
content: (
|
||||
<div>
|
||||
<span className="mr-4">
|
||||
{moment(StudyDate).format('MMM-DD-YYYY')}
|
||||
</span>
|
||||
<span>{moment(StudyDate).format('hh:mm A')}</span>
|
||||
</div>
|
||||
),
|
||||
gridCol: 5,
|
||||
},
|
||||
{
|
||||
key: 'description',
|
||||
content: StudyDescription,
|
||||
gridCol: 4,
|
||||
},
|
||||
{
|
||||
key: 'modality',
|
||||
content: Modalities,
|
||||
gridCol: 3,
|
||||
},
|
||||
{
|
||||
key: 'accession',
|
||||
content: AccessionNumber,
|
||||
gridCol: 4,
|
||||
},
|
||||
{
|
||||
key: 'instances',
|
||||
content: (
|
||||
<>
|
||||
<Icon
|
||||
name="series-active"
|
||||
className={classnames('inline-flex mr-2', {
|
||||
'text-primary-active': isExpanded,
|
||||
'text-secondary-light': !isExpanded,
|
||||
})}
|
||||
/>
|
||||
{Instances}
|
||||
</>
|
||||
),
|
||||
gridCol: 4,
|
||||
},
|
||||
],
|
||||
expandedContent: (
|
||||
<StudyListExpandedRow
|
||||
seriesTableColumns={seriesTableColumns}
|
||||
seriesTableDataSource={seriesTableDataSource}
|
||||
>
|
||||
<Button
|
||||
rounded="full"
|
||||
variant="contained"
|
||||
className="mr-4 font-bold"
|
||||
endIcon={<Icon name="launch-arrow" style={{ color: '#21a7c6' }} />}
|
||||
>
|
||||
Basic Viewer
|
||||
</Button>
|
||||
<Button
|
||||
rounded="full"
|
||||
variant="contained"
|
||||
className="mr-4 font-bold"
|
||||
endIcon={<Icon name="launch-arrow" style={{ color: '#21a7c6' }} />}
|
||||
>
|
||||
Segmentation
|
||||
</Button>
|
||||
<Button
|
||||
rounded="full"
|
||||
variant="outlined"
|
||||
endIcon={<Icon name="launch-info" />}
|
||||
className="font-bold"
|
||||
>
|
||||
Module 3
|
||||
</Button>
|
||||
<div className="ml-5 text-lg text-common-bright inline-flex items-center">
|
||||
<Icon name="notificationwarning-diamond" className="mr-2 w-5 h-5" />
|
||||
Feedback text lorem ipsum dolor sit amet
|
||||
</div>
|
||||
</StudyListExpandedRow>
|
||||
),
|
||||
onClickRow: () =>
|
||||
setExpandedRows(s =>
|
||||
isExpanded ? s.filter(n => rowKey !== n) : [...s, rowKey]
|
||||
),
|
||||
isExpanded,
|
||||
};
|
||||
});
|
||||
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [perPage, setPerPage] = useState(25);
|
||||
const totalPages = Math.floor(numOfStudies / perPage);
|
||||
const onChangePage = page => {
|
||||
if (page > totalPages) {
|
||||
return;
|
||||
}
|
||||
setCurrentPage(page);
|
||||
};
|
||||
const onChangePerPage = perPage => {
|
||||
setPerPage(perPage);
|
||||
setCurrentPage(1);
|
||||
};
|
||||
|
||||
const hasStudies = numOfStudies > 0;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classnames('bg-black h-full', {
|
||||
'h-screen': !hasStudies,
|
||||
})}
|
||||
>
|
||||
<Header />
|
||||
<StudyListFilter
|
||||
numOfStudies={numOfStudies}
|
||||
filtersMeta={filtersMeta}
|
||||
filterValues={filterValues}
|
||||
onChange={setFilterValues}
|
||||
clearFilters={() => setFilterValues(defaultFilterValues)}
|
||||
isFiltering={isFiltering(filterValues, defaultFilterValues)}
|
||||
/>
|
||||
|
||||
{hasStudies ? (
|
||||
<>
|
||||
<StudyListTable
|
||||
tableDataSource={tableDataSource}
|
||||
numOfStudies={numOfStudies}
|
||||
filtersMeta={filtersMeta}
|
||||
/>
|
||||
<StudyListPagination
|
||||
onChangePage={onChangePage}
|
||||
onChangePerPage={onChangePerPage}
|
||||
currentPage={currentPage}
|
||||
perPage={perPage}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center pt-48">
|
||||
<EmptyStudies />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default StudyList;
|
||||
@ -12,6 +12,8 @@ import { useAppConfig } from '@state';
|
||||
import { useDebounce, useQuery } from '@hooks';
|
||||
import { utils } from '@ohif/core';
|
||||
|
||||
const { sortBySeriesDate, hotkeys } = utils;
|
||||
|
||||
import {
|
||||
Icon,
|
||||
StudyListExpandedRow,
|
||||
@ -194,7 +196,7 @@ function WorkList({
|
||||
const series = await dataSource.query.series.search(studyInstanceUid);
|
||||
seriesInStudiesMap.set(
|
||||
studyInstanceUid,
|
||||
utils.sortBySeriesDate(series)
|
||||
sortBySeriesDate(series)
|
||||
);
|
||||
setStudiesWithSeriesData([...studiesWithSeriesData, studyInstanceUid]);
|
||||
} catch (ex) {
|
||||
@ -399,6 +401,7 @@ function WorkList({
|
||||
hide();
|
||||
},
|
||||
onReset: () => hotkeysManager.restoreDefaultBindings(),
|
||||
hotkeysModule: hotkeys
|
||||
},
|
||||
}),
|
||||
},
|
||||
|
||||
21
yarn.lock
21
yarn.lock
@ -1114,27 +1114,13 @@
|
||||
core-js-pure "^3.15.0"
|
||||
regenerator-runtime "^0.13.4"
|
||||
|
||||
"@babel/runtime@7.1.2":
|
||||
version "7.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.1.2.tgz#81c89935f4647706fc54541145e6b4ecfef4b8e3"
|
||||
integrity sha512-Y3SCjmhSupzFB6wcv1KmmFucH6gDVnI30WjOcicV10ju0cZjak3Jcs67YLIXBrmZYw1xCrVeJPbycFwrqNyxpg==
|
||||
dependencies:
|
||||
regenerator-runtime "^0.12.0"
|
||||
|
||||
"@babel/runtime@7.7.6", "@babel/runtime@^7.1.2", "@babel/runtime@^7.2.0", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.4", "@babel/runtime@^7.7.6":
|
||||
"@babel/runtime@7.1.2", "@babel/runtime@7.7.6", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.10.3", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.5", "@babel/runtime@^7.2.0", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.4", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2":
|
||||
version "7.7.6"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.7.6.tgz#d18c511121aff1b4f2cd1d452f1bac9601dd830f"
|
||||
integrity sha512-BWAJxpNVa0QlE5gZdWjSxXtemZyZ9RmrmVozxt3NUXeZhVIJ5ANyqmMc0JDrivBZyxUuQvFxlvH4OWWOogGfUw==
|
||||
dependencies:
|
||||
regenerator-runtime "^0.13.2"
|
||||
|
||||
"@babel/runtime@^7.10.2", "@babel/runtime@^7.10.3", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.5", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2":
|
||||
version "7.14.6"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.14.6.tgz#535203bc0892efc7dec60bdc27b2ecf6e409062d"
|
||||
integrity sha512-/PCB2uJ7oM44tz8YhC4Z/6PeOKXp4K588f+5M3clr1M4zbqztlo0XEfJ2LEzj/FgwfgGcIdl8n7YYjTCI0BYwg==
|
||||
dependencies:
|
||||
regenerator-runtime "^0.13.4"
|
||||
|
||||
"@babel/template@^7.12.7", "@babel/template@^7.14.5", "@babel/template@^7.4.0":
|
||||
version "7.14.5"
|
||||
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.14.5.tgz#a9bc9d8b33354ff6e55a9c60d1109200a68974f4"
|
||||
@ -15604,11 +15590,6 @@ regenerate@^1.4.0:
|
||||
resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.2.tgz#b9346d8827e8f5a32f7ba29637d398b69014848a"
|
||||
integrity sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==
|
||||
|
||||
regenerator-runtime@^0.12.0:
|
||||
version "0.12.1"
|
||||
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.12.1.tgz#fa1a71544764c036f8c49b13a08b2594c9f8a0de"
|
||||
integrity sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg==
|
||||
|
||||
regenerator-runtime@^0.13.1, regenerator-runtime@^0.13.2, regenerator-runtime@^0.13.3, regenerator-runtime@^0.13.4:
|
||||
version "0.13.7"
|
||||
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz#cac2dacc8a1ea675feaabaeb8ae833898ae46f55"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user