Try to get views to work for component library
This commit is contained in:
parent
ff28a2264b
commit
ca7506c5f0
14
platform/core/src/utils/hotkeys/index.js
Normal file
14
platform/core/src/utils/hotkeys/index.js
Normal file
@ -0,0 +1,14 @@
|
||||
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;
|
||||
32
platform/core/src/utils/hotkeys/pausePlugin.js
Normal file
32
platform/core/src/utils/hotkeys/pausePlugin.js
Normal file
@ -0,0 +1,32 @@
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
218
platform/core/src/utils/hotkeys/recordPlugin.js
Normal file
218
platform/core/src/utils/hotkeys/recordPlugin.js
Normal file
@ -0,0 +1,218 @@
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
@ -61,7 +61,7 @@ window.config = {
|
||||
> data!
|
||||
>
|
||||
> You can read more about data sources at
|
||||
> [Data Source section in Modes](../modes/index.md)
|
||||
> [Data Source section in Modes](../platform/modes/index.md)
|
||||
|
||||
The configuration can also be written as a JS Function in case you need to
|
||||
inject dependencies like external services:
|
||||
|
||||
@ -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](../../configuration/index.md).
|
||||
[configuration essentials guide](../configuration/index.md).
|
||||
|
||||
## Next Steps
|
||||
|
||||
|
||||
@ -0,0 +1,345 @@
|
||||
---
|
||||
name: Study List
|
||||
menu: Views
|
||||
route: examples/studyList
|
||||
---
|
||||
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
Icon,
|
||||
StudyListExpandedRow,
|
||||
Button,
|
||||
NavBar,
|
||||
Svg,
|
||||
IconButton,
|
||||
EmptyStudies,
|
||||
StudyListTable,
|
||||
StudyListPagination,
|
||||
StudyListFilter,
|
||||
} from '@ohif/ui';
|
||||
import utils from '../../../../src/utils';
|
||||
|
||||
import classnames from 'classnames';
|
||||
import moment from 'moment';
|
||||
|
||||
# Study List
|
||||
|
||||
This example shows you how you can build a Study List page using the available
|
||||
components.
|
||||
|
||||
```jsx live
|
||||
function () {
|
||||
const defaultFilterValues = {
|
||||
patientName: '',
|
||||
mrn: '',
|
||||
studyDate: {
|
||||
startDate: null,
|
||||
endDate: null,
|
||||
},
|
||||
description: '',
|
||||
modality: undefined,
|
||||
accession: '',
|
||||
sortBy: '',
|
||||
sortDirection: 'none',
|
||||
page: 0,
|
||||
resultsPerPage: 25,
|
||||
};
|
||||
const [filterValues, setFilterValues] = useState(defaultFilterValues);
|
||||
const [studies, setStudies] = useState([]);
|
||||
const numOfStudies = studies.length;
|
||||
const [expandedRows, setExpandedRows] = useState([]);
|
||||
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 isFiltering = (filterValues, defaultFilterValues) => {
|
||||
return Object.keys(defaultFilterValues).some(name => {
|
||||
return filterValues[name] !== defaultFilterValues[name];
|
||||
});
|
||||
};
|
||||
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="group-layers"
|
||||
className={classnames('inline-flex mr-2 w-4', {
|
||||
'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 handleStudyList = number => {
|
||||
const studies = utils.getMockedStudies(number);
|
||||
setStudies(studies);
|
||||
setCurrentPage(1);
|
||||
};
|
||||
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>
|
||||
<div className="bg-white p-4">
|
||||
<Button className="mr-2" onClick={() => handleStudyList(0)}>
|
||||
0 Studies
|
||||
</Button>
|
||||
<Button className="mr-2" onClick={() => handleStudyList(50)}>
|
||||
50 Studies
|
||||
</Button>
|
||||
<Button className="mr-2" onClick={() => handleStudyList(100)}>
|
||||
100 Studies
|
||||
</Button>
|
||||
<Button className="mr-2" onClick={() => handleStudyList(1000)}>
|
||||
1000 Studies
|
||||
</Button>
|
||||
</div>
|
||||
<div
|
||||
className={classnames('bg-black h-full', {
|
||||
'h-screen': !hasStudies,
|
||||
})}
|
||||
>
|
||||
<NavBar className="justify-between border-b-4 border-black" isSticky>
|
||||
<div className="flex items-center">
|
||||
<div className="mx-3">
|
||||
<Svg name="logo-ohif" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<span className="mr-3 text-common-light text-lg">
|
||||
FOR INVESTIGATIONAL USE ONLY
|
||||
</span>
|
||||
<IconButton
|
||||
variant="text"
|
||||
color="inherit"
|
||||
className="text-primary-active"
|
||||
onClick={() => {}}
|
||||
>
|
||||
<React.Fragment>
|
||||
<Icon name="settings" />
|
||||
<Icon name="chevron-down" />
|
||||
</React.Fragment>
|
||||
</IconButton>
|
||||
</div>
|
||||
</NavBar>
|
||||
<StudyListFilter
|
||||
numOfStudies={numOfStudies}
|
||||
filtersMeta={filtersMeta}
|
||||
filterValues={filterValues}
|
||||
onChange={setFilterValues}
|
||||
clearFilters={() => setFilterValues(defaultFilterValues)}
|
||||
isFiltering={isFiltering(filterValues, defaultFilterValues)}
|
||||
/>
|
||||
{hasStudies ? (
|
||||
<>
|
||||
<StudyListTable
|
||||
tableDataSource={tableDataSource.slice(
|
||||
(currentPage - 1) * perPage,
|
||||
(currentPage - 1) * perPage + perPage
|
||||
)}
|
||||
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>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
@ -0,0 +1,4 @@
|
||||
{
|
||||
"label": "Example Views",
|
||||
"position": 6
|
||||
}
|
||||
@ -1,346 +0,0 @@
|
||||
---
|
||||
name: Study List
|
||||
menu: Views
|
||||
route: examples/studyList
|
||||
---
|
||||
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
Icon,
|
||||
StudyListExpandedRow,
|
||||
Button,
|
||||
NavBar,
|
||||
Svg,
|
||||
IconButton,
|
||||
EmptyStudies,
|
||||
StudyListTable,
|
||||
StudyListPagination,
|
||||
StudyListFilter
|
||||
} from '@ohif/ui';
|
||||
import utils from '../../utils';
|
||||
|
||||
|
||||
import classnames from 'classnames';
|
||||
import moment from 'moment';
|
||||
|
||||
# Study List
|
||||
|
||||
This example shows you how you can build a Study List page using the available
|
||||
components.
|
||||
|
||||
```jsx live
|
||||
{() => {
|
||||
const defaultFilterValues = {
|
||||
patientName: '',
|
||||
mrn: '',
|
||||
studyDate: {
|
||||
startDate: null,
|
||||
endDate: null,
|
||||
},
|
||||
description: '',
|
||||
modality: undefined,
|
||||
accession: '',
|
||||
sortBy: '',
|
||||
sortDirection: 'none',
|
||||
page: 0,
|
||||
resultsPerPage: 25,
|
||||
};
|
||||
const [filterValues, setFilterValues] = useState(defaultFilterValues);
|
||||
const [studies, setStudies] = useState([]);
|
||||
const numOfStudies = studies.length;
|
||||
const [expandedRows, setExpandedRows] = useState([]);
|
||||
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 isFiltering = (filterValues, defaultFilterValues) => {
|
||||
return Object.keys(defaultFilterValues).some((name) => {
|
||||
return filterValues[name] !== defaultFilterValues[name];
|
||||
});
|
||||
};
|
||||
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="group-layers"
|
||||
className={classnames('inline-flex mr-2 w-4', {
|
||||
'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 handleStudyList = (number) => {
|
||||
const studies = utils.getMockedStudies(number);
|
||||
setStudies(studies);
|
||||
setCurrentPage(1);
|
||||
};
|
||||
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>
|
||||
<div className="bg-white p-4">
|
||||
<Button className="mr-2" onClick={() => handleStudyList(0)}>
|
||||
0 Studies
|
||||
</Button>
|
||||
<Button className="mr-2" onClick={() => handleStudyList(50)}>
|
||||
50 Studies
|
||||
</Button>
|
||||
<Button className="mr-2" onClick={() => handleStudyList(100)}>
|
||||
100 Studies
|
||||
</Button>
|
||||
<Button className="mr-2" onClick={() => handleStudyList(1000)}>
|
||||
1000 Studies
|
||||
</Button>
|
||||
</div>
|
||||
<div
|
||||
className={classnames('bg-black h-full', {
|
||||
'h-screen': !hasStudies,
|
||||
})}
|
||||
>
|
||||
<NavBar className="justify-between border-b-4 border-black" isSticky>
|
||||
<div className="flex items-center">
|
||||
<div className="mx-3">
|
||||
<Svg name="logo-ohif" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<span className="mr-3 text-common-light text-lg">
|
||||
FOR INVESTIGATIONAL USE ONLY
|
||||
</span>
|
||||
<IconButton
|
||||
variant="text"
|
||||
color="inherit"
|
||||
className="text-primary-active"
|
||||
onClick={() => {}}
|
||||
>
|
||||
<React.Fragment>
|
||||
<Icon name="settings" />
|
||||
<Icon name="chevron-down" />
|
||||
</React.Fragment>
|
||||
</IconButton>
|
||||
</div>
|
||||
</NavBar>
|
||||
<StudyListFilter
|
||||
numOfStudies={numOfStudies}
|
||||
filtersMeta={filtersMeta}
|
||||
filterValues={filterValues}
|
||||
onChange={setFilterValues}
|
||||
clearFilters={() => setFilterValues(defaultFilterValues)}
|
||||
isFiltering={isFiltering(filterValues, defaultFilterValues)}
|
||||
/>
|
||||
{hasStudies ? (
|
||||
<>
|
||||
<StudyListTable
|
||||
tableDataSource={tableDataSource.slice(
|
||||
(currentPage - 1) * perPage,
|
||||
(currentPage - 1) * perPage + perPage
|
||||
)}
|
||||
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>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
```
|
||||
@ -2,23 +2,22 @@
|
||||
sidebar_position: 2
|
||||
sidebar_label: Theming
|
||||
---
|
||||
|
||||
# Viewer: Theming
|
||||
|
||||
|
||||
`OHIF-v3` has introduced the [`LayoutTemplateModule`](../extensions/modules/layout-template.md) which enables addition of custom layouts. You can easily design your custom components inside an extension and consume it via the layoutTemplate module you write.
|
||||
|
||||
|
||||
|
||||
`OHIF-v3` has introduced the
|
||||
[`LayoutTemplateModule`](./extensions/modules/layout-template.md) which enables
|
||||
addition of custom layouts. You can easily design your custom components inside
|
||||
an extension and consume it via the layoutTemplate module you write.
|
||||
|
||||
## Tailwind CSS
|
||||
[Tailwind CSS](https://tailwindcss.com/) is a utility-first CSS framework for creating custom user interfaces.
|
||||
|
||||
|
||||
Below you can see a compiled version of the tailwind configs.
|
||||
Each section can be edited accordingly. For instance screen size break points, primary
|
||||
and secondary colors, etc.
|
||||
|
||||
[Tailwind CSS](https://tailwindcss.com/) is a utility-first CSS framework for
|
||||
creating custom user interfaces.
|
||||
|
||||
Below you can see a compiled version of the tailwind configs. Each section can
|
||||
be edited accordingly. For instance screen size break points, primary and
|
||||
secondary colors, etc.
|
||||
|
||||
```js
|
||||
module.exports = {
|
||||
@ -79,13 +78,11 @@ module.exports = {
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
|
||||
You can also use the color variable like before. For instance:
|
||||
|
||||
|
||||
```js
|
||||
primary: {
|
||||
default: ‘var(--default-color)‘,
|
||||
@ -96,19 +93,22 @@ primary: {
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## White Labeling
|
||||
|
||||
A white-label product is a product or service produced by one company (the producer) that other companies (the marketers) rebrand to make it appear as if they had made it - [Wikipedia: White-Label Product](https://en.wikipedia.org/wiki/White-label_product)
|
||||
A white-label product is a product or service produced by one company (the
|
||||
producer) that other companies (the marketers) rebrand to make it appear as if
|
||||
they had made it -
|
||||
[Wikipedia: White-Label Product](https://en.wikipedia.org/wiki/White-label_product)
|
||||
|
||||
Current white-labeling options are limited.
|
||||
We expose the ability to replace the "Logo" section of the application with a custom "Logo" component. You can do this by adding a whiteLabeling key to your configuration file.
|
||||
Current white-labeling options are limited. We expose the ability to replace the
|
||||
"Logo" section of the application with a custom "Logo" component. You can do
|
||||
this by adding a whiteLabeling key to your configuration file.
|
||||
|
||||
```js
|
||||
window.config = {
|
||||
/** .. **/
|
||||
whiteLabeling: {
|
||||
createLogoComponentFn: function (React) {
|
||||
createLogoComponentFn: function(React) {
|
||||
return React.createElement(
|
||||
'a',
|
||||
{
|
||||
@ -118,24 +118,22 @@ window.config = {
|
||||
href: 'http://radicalimaging.com',
|
||||
},
|
||||
React.createElement('h5', {}, 'RADICAL IMAGING')
|
||||
)
|
||||
);
|
||||
},
|
||||
},
|
||||
/** .. **/
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
> You can simply use the stylings from tailwind CSS in the whiteLabeling
|
||||
|
||||
|
||||
In addition to text, you can also add your custom logo
|
||||
|
||||
|
||||
```js
|
||||
window.config = {
|
||||
/** .. **/
|
||||
whiteLabeling: {
|
||||
createLogoComponentFn: function (React) {
|
||||
createLogoComponentFn: function(React) {
|
||||
return React.createElement(
|
||||
'a',
|
||||
{
|
||||
@ -148,20 +146,17 @@ window.config = {
|
||||
src: './customLogo.svg',
|
||||
// className: 'w-8 h-8',
|
||||
})
|
||||
)
|
||||
);
|
||||
},
|
||||
},
|
||||
/** .. **/
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
The output will look like
|
||||
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
<!--
|
||||
Links
|
||||
-->
|
||||
|
||||
@ -35,6 +35,7 @@
|
||||
"@mdx-js/react": "^1.6.21",
|
||||
"@ohif/ui": "^2.0.0",
|
||||
"@svgr/webpack": "^5.5.0",
|
||||
"classnames": "^2.3.1",
|
||||
"clsx": "^1.1.1",
|
||||
"file-loader": "^6.2.0",
|
||||
"plugin-image-zoom": "ataft/plugin-image-zoom",
|
||||
|
||||
@ -6,13 +6,19 @@
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import classnames from 'classnames';
|
||||
import moment from 'moment';
|
||||
import * as ui from '@ohif/ui';
|
||||
import utils from '../../utils/';
|
||||
|
||||
// Add react-live imports you need here
|
||||
const ReactLiveScope = {
|
||||
React,
|
||||
...React,
|
||||
...ui,
|
||||
classnames,
|
||||
utils,
|
||||
moment,
|
||||
};
|
||||
|
||||
export default ReactLiveScope;
|
||||
|
||||
17
platform/docs/src/utils/getMockedStudies.js
Normal file
17
platform/docs/src/utils/getMockedStudies.js
Normal file
@ -0,0 +1,17 @@
|
||||
import studyListMock from '../mocks/studyList';
|
||||
|
||||
/** 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;
|
||||
7
platform/docs/src/utils/index.js
Normal file
7
platform/docs/src/utils/index.js
Normal file
@ -0,0 +1,7 @@
|
||||
import getMockedStudies from './getMockedStudies';
|
||||
|
||||
const utils = { getMockedStudies };
|
||||
|
||||
export { getMockedStudies };
|
||||
|
||||
export default utils;
|
||||
@ -56,7 +56,7 @@ HotkeyField.propTypes = {
|
||||
className: PropTypes.string,
|
||||
modifierKeys: PropTypes.array,
|
||||
disabled: PropTypes.bool,
|
||||
hotkeys: PropTypes.object({
|
||||
hotkeys: PropTypes.shape({
|
||||
initialize: PropTypes.func.isRequired,
|
||||
pause: PropTypes.func.isRequired,
|
||||
unpause: PropTypes.func.isRequired,
|
||||
|
||||
@ -107,7 +107,7 @@ HotkeysPreferences.propTypes = {
|
||||
onChange: PropTypes.func,
|
||||
disabled: PropTypes.bool,
|
||||
hotkeyDefinitions: PropTypes.object.isRequired,
|
||||
hotkeysModule: PropTypes.object({
|
||||
hotkeysModule: PropTypes.shape({
|
||||
initialize: PropTypes.func.isRequired,
|
||||
pause: PropTypes.func.isRequired,
|
||||
unpause: PropTypes.func.isRequired,
|
||||
|
||||
@ -126,7 +126,7 @@ UserPreferences.propTypes = {
|
||||
onCancel: PropTypes.func,
|
||||
onSubmit: PropTypes.func,
|
||||
onReset: PropTypes.func,
|
||||
hotkeysModule: PropTypes.object({
|
||||
hotkeysModule: PropTypes.shape({
|
||||
initialize: PropTypes.func.isRequired,
|
||||
pause: PropTypes.func.isRequired,
|
||||
unpause: PropTypes.func.isRequired,
|
||||
|
||||
23
yarn.lock
23
yarn.lock
@ -1114,13 +1114,27 @@
|
||||
core-js-pure "^3.15.0"
|
||||
regenerator-runtime "^0.13.4"
|
||||
|
||||
"@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":
|
||||
"@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":
|
||||
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.8"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.14.8.tgz#7119a56f421018852694290b9f9148097391b446"
|
||||
integrity sha512-twj3L8Og5SaCRCErB4x4ajbvBIVV77CGeFglHpeg5WC5FF8TZzBWXtTJ4MqaD9QszLYTtr+IsaAL2rEUevb+eg==
|
||||
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"
|
||||
@ -5450,7 +5464,7 @@ classnames@2.2.6:
|
||||
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.6.tgz#43935bffdd291f326dad0a205309b38d00f650ce"
|
||||
integrity sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q==
|
||||
|
||||
classnames@^2.2.5, classnames@^2.2.6:
|
||||
classnames@^2.2.5, classnames@^2.2.6, classnames@^2.3.1:
|
||||
version "2.3.1"
|
||||
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.1.tgz#dfcfa3891e306ec1dad105d0e88f4417b8535e8e"
|
||||
integrity sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==
|
||||
@ -15590,6 +15604,11 @@ 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