Merge branch 'feat/v2-main' of https://github.com/OHIF/Viewers into feat/ui-v2-Dropdown
This commit is contained in:
commit
0d53346d61
73 files changed
+1423
-369
No files matched your search
@@ -31,14 +31,14 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"cornerstone-core": "^2.3.0",
|
||||
"cornerstone-tools": "^4.12.0",
|
||||
"cornerstone-tools": "4.16.0",
|
||||
"cornerstone-wado-image-loader": "^3.1.2",
|
||||
"dicom-parser": "^1.8.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "7.7.6",
|
||||
"ajv": "^6.10.0",
|
||||
"dcmjs": "^0.12.4",
|
||||
"dcmjs": "0.14.0",
|
||||
"dicomweb-client": "^0.6.0",
|
||||
"immer": "6.0.2",
|
||||
"isomorphic-base64": "^1.0.2",
|
||||
|
||||
@@ -6,6 +6,10 @@ import {
|
||||
stowSRFromMeasurements,
|
||||
} from './handleStructuredReport';
|
||||
import findMostRecentStructuredReport from './utils/findMostRecentStructuredReport';
|
||||
import cornerstoneTools from 'cornerstone-tools';
|
||||
import dcmjs from 'dcmjs';
|
||||
|
||||
const { MeasurementReport } = dcmjs.adapters.Cornerstone;
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -47,7 +51,7 @@ const retrieveMeasurements = server => {
|
||||
* @param {serverType} server
|
||||
* @returns {Object} With message to be displayed on success
|
||||
*/
|
||||
const storeMeasurements = async (measurementData, filter, server) => {
|
||||
const storeMeasurementsOld = async (measurementData, filter, server) => {
|
||||
log.info('[DICOMSR] storeMeasurements');
|
||||
|
||||
if (!server || server.type !== 'dicomWeb') {
|
||||
@@ -78,4 +82,128 @@ const storeMeasurements = async (measurementData, filter, server) => {
|
||||
}
|
||||
};
|
||||
|
||||
export { retrieveMeasurements, storeMeasurements };
|
||||
/**
|
||||
*
|
||||
* @param {object[]} measurementData An array of measurements from the measurements service
|
||||
* that you wish to serialize.
|
||||
*/
|
||||
const downloadReport = measurementData => {
|
||||
const srDataset = generateReport(measurementData);
|
||||
const reportBlob = dcmjs.data.datasetToBlob(srDataset);
|
||||
|
||||
//Create a URL for the binary.
|
||||
var objectUrl = URL.createObjectURL(reportBlob);
|
||||
window.location.assign(objectUrl);
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {object[]} measurementData An array of measurements from the measurements service
|
||||
* that you wish to serialize.
|
||||
*/
|
||||
const generateReport = measurementData => {
|
||||
const ids = measurementData.map(md => md.id);
|
||||
const filteredToolState = _getFilteredCornerstoneToolState(ids);
|
||||
|
||||
const report = MeasurementReport.generateReport(
|
||||
filteredToolState,
|
||||
cornerstone.metaData
|
||||
);
|
||||
|
||||
return report.dataset;
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {object[]} measurementData An array of measurements from the measurements service
|
||||
* that you wish to serialize.
|
||||
* @param {object} dataSource The dataSource that you wish to use to persist the data.
|
||||
*/
|
||||
const storeMeasurements = async (measurementData, dataSource) => {
|
||||
// TODO -> Eventually use the measurements directly and not the dcmjs adapter,
|
||||
// But it is good enough for now whilst we only have cornerstone as a datasource.
|
||||
log.info('[DICOMSR] storeMeasurements');
|
||||
|
||||
if (!dataSource || !dataSource.store || !dataSource.store.dicom) {
|
||||
log.error('[DICOMSR] datasource has no dataSource.store.dicom endpoint!');
|
||||
return Promise.reject({});
|
||||
}
|
||||
|
||||
const naturalizedReport = generateReport(measurementData);
|
||||
const { StudyInstanceUID } = naturalizedReport;
|
||||
|
||||
try {
|
||||
await dataSource.store.dicom(naturalizedReport);
|
||||
|
||||
if (StudyInstanceUID) {
|
||||
studies.deleteStudyMetadataPromise(StudyInstanceUID);
|
||||
}
|
||||
|
||||
return {
|
||||
message: 'Measurements saved successfully',
|
||||
};
|
||||
} catch (error) {
|
||||
log.error(
|
||||
`[DICOMSR] Error while saving the measurements: ${error.message}`
|
||||
);
|
||||
throw new Error('Error while saving the measurements.');
|
||||
}
|
||||
};
|
||||
|
||||
function _getFilteredCornerstoneToolState(uidFilter) {
|
||||
const globalToolState = cornerstoneTools.globalImageIdSpecificToolStateManager.saveToolState();
|
||||
const filteredToolState = {};
|
||||
|
||||
function addToFilteredToolState(imageId, toolType, toolDataI) {
|
||||
if (!filteredToolState[imageId]) {
|
||||
filteredToolState[imageId] = {};
|
||||
}
|
||||
|
||||
const imageIdSpecificToolState = filteredToolState[imageId];
|
||||
|
||||
if (!imageIdSpecificToolState[toolType]) {
|
||||
imageIdSpecificToolState[toolType] = {
|
||||
data: [],
|
||||
};
|
||||
}
|
||||
|
||||
const toolData = imageIdSpecificToolState[toolType].data;
|
||||
|
||||
toolData.push(toolDataI);
|
||||
}
|
||||
|
||||
const uids = uidFilter.slice();
|
||||
const imageIds = Object.keys(globalToolState);
|
||||
|
||||
for (let i = 0; i < imageIds.length; i++) {
|
||||
const imageId = imageIds[i];
|
||||
const imageIdSpecificToolState = globalToolState[imageId];
|
||||
|
||||
const toolTypes = Object.keys(imageIdSpecificToolState);
|
||||
|
||||
for (let j = 0; j < toolTypes.length; j++) {
|
||||
const toolType = toolTypes[j];
|
||||
const toolData = imageIdSpecificToolState[toolType].data;
|
||||
|
||||
if (toolData) {
|
||||
for (let k = 0; k < toolData.length; k++) {
|
||||
const toolDataK = toolData[k];
|
||||
const uidIndex = uids.findIndex(uid => uid === toolDataK.id);
|
||||
|
||||
if (uidIndex !== -1) {
|
||||
addToFilteredToolState(imageId, toolType, toolDataK);
|
||||
uids.splice(uidIndex, 1);
|
||||
|
||||
if (!uids.length) {
|
||||
return filteredToolState;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return filteredToolState;
|
||||
}
|
||||
|
||||
export { retrieveMeasurements, storeMeasurements, downloadReport };
|
||||
@@ -1,9 +1,14 @@
|
||||
import { retrieveMeasurements, storeMeasurements } from './dataExchange';
|
||||
import {
|
||||
retrieveMeasurements,
|
||||
storeMeasurements,
|
||||
downloadReport,
|
||||
} from './dataExchange';
|
||||
import isToolSupported from './utils/isToolSupported';
|
||||
|
||||
const DICOMSR = {
|
||||
retrieveMeasurements,
|
||||
storeMeasurements,
|
||||
downloadReport,
|
||||
isToolSupported,
|
||||
};
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import { DicomMetadataStore } from '@ohif/core';
|
||||
function create({
|
||||
query,
|
||||
retrieve,
|
||||
store,
|
||||
retrieveSeriesMetadata,
|
||||
getImageIdsForDisplaySet,
|
||||
}) {
|
||||
@@ -44,13 +45,20 @@ function create({
|
||||
series: {},
|
||||
};
|
||||
|
||||
const defaultStore = {
|
||||
dicom: async naturalizedDataset => {
|
||||
throw new Error(
|
||||
'store.dicom(naturalizedDicom, StudyInstanceUID) not implemented for dataSource.'
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
query: query || defaultQuery,
|
||||
retrieve: retrieve || defaultRetrieve,
|
||||
store: store || defaultStore,
|
||||
getImageIdsForDisplaySet,
|
||||
retrieveSeriesMetadata,
|
||||
// then go get all series level metadata.
|
||||
// Store this in the DICOM MetadataStore.
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -137,6 +137,7 @@ export default class ExtensionManager {
|
||||
|
||||
getDataSources = dataSourceName => {
|
||||
if (dataSourceName === undefined) {
|
||||
// Default to the activeDataSource
|
||||
dataSourceName = this.activeDataSource;
|
||||
}
|
||||
|
||||
@@ -144,6 +145,10 @@ export default class ExtensionManager {
|
||||
return this.dataSourceMap[dataSourceName];
|
||||
};
|
||||
|
||||
getActiveDataSource = () => {
|
||||
return this.activeDataSource;
|
||||
};
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {string} moduleType
|
||||
|
||||
@@ -40,6 +40,10 @@ const MEASUREMENT_SCHEMA_KEYS = [
|
||||
'type',
|
||||
'unit',
|
||||
'area', // TODO: Add concept names instead (descriptor)
|
||||
'length',
|
||||
'shortestDiameter',
|
||||
'longestDiameter',
|
||||
'text', // NOTE: There is nothing like this in SR.
|
||||
'points',
|
||||
'source',
|
||||
];
|
||||
@@ -53,6 +57,7 @@ const EVENTS = {
|
||||
const VALUE_TYPES = {
|
||||
POLYLINE: 'value_type::polyline',
|
||||
POINT: 'value_type::point',
|
||||
BIDIRECTIONAL: 'value_type::shortAxisLongAxis', // TODO -> Discuss with Danny. => just using SCOORD values isn't enough here.
|
||||
ELLIPSE: 'value_type::ellipse',
|
||||
MULTIPOINT: 'value_type::multipoint',
|
||||
CIRCLE: 'value_type::circle',
|
||||
|
||||
@@ -62,7 +62,7 @@ export default class ToolBarService {
|
||||
this._broadcastChange(this.EVENTS.TOOL_BAR_MODIFIED, {});
|
||||
}
|
||||
|
||||
getButtonSection(key) {
|
||||
getButtonSection(key, props) {
|
||||
const buttonSectionIds = this.buttonSections[key];
|
||||
const buttonsInSection = [];
|
||||
|
||||
@@ -79,7 +79,8 @@ export default class ToolBarService {
|
||||
|
||||
btnIds.forEach(nestedBtnId => {
|
||||
const nestedBtn = this.buttons[nestedBtnId];
|
||||
const mappedNestedBtn = this._mapButtonToDisplay(nestedBtn, key);
|
||||
const metadata = { isNested: true };
|
||||
const mappedNestedBtn = this._mapButtonToDisplay(nestedBtn, key, metadata, props);
|
||||
|
||||
nestedButtons.push(mappedNestedBtn);
|
||||
});
|
||||
@@ -90,7 +91,8 @@ export default class ToolBarService {
|
||||
} else {
|
||||
const btnId = btnIdOrArray;
|
||||
const btn = this.buttons[btnId];
|
||||
const mappedBtn = this._mapButtonToDisplay(btn, key);
|
||||
const metadata = { isNested: false };
|
||||
const mappedBtn = this._mapButtonToDisplay(btn, key, metadata, props);
|
||||
|
||||
buttonsInSection.push(mappedBtn);
|
||||
}
|
||||
@@ -136,8 +138,8 @@ export default class ToolBarService {
|
||||
* @param {*} btn
|
||||
* @param {*} btnSection
|
||||
*/
|
||||
_mapButtonToDisplay(btn, btnSection) {
|
||||
const { id, type, component, props } = btn;
|
||||
_mapButtonToDisplay(btn, btnSection, metadata, props) {
|
||||
const { id, type, component } = btn;
|
||||
const buttonType = this._buttonTypes()[type];
|
||||
|
||||
if (!buttonType) {
|
||||
@@ -146,7 +148,7 @@ export default class ToolBarService {
|
||||
|
||||
const onClick = evt => {
|
||||
if (buttonType.clickHandler) {
|
||||
buttonType.clickHandler(evt, btn, btnSection);
|
||||
buttonType.clickHandler(evt, btn, btnSection, metadata, props);
|
||||
}
|
||||
if (btn.props.onClick) {
|
||||
btn.onClick(evt, btn, btnSection);
|
||||
@@ -154,12 +156,15 @@ export default class ToolBarService {
|
||||
if (btn.props.clickHandler) {
|
||||
btn.clickHandler(evt, btn, btnSection);
|
||||
}
|
||||
if (props && props.onClick) {
|
||||
props.onClick(evt, btn, btnSection, props);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
id,
|
||||
Component: component || buttonType.defaultComponent,
|
||||
componentProps: Object.assign({}, props, { onClick }), //
|
||||
componentProps: Object.assign({}, btn.props, { onClick }), //
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* Formats a patient name for display purposes
|
||||
*/
|
||||
export default function formatPN(name) {
|
||||
if (!name) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Convert the first ^ to a ', '. String.replace() only affects
|
||||
// the first appearance of the character.
|
||||
const commaBetweenFirstAndLast = name.replace('^', ', ');
|
||||
|
||||
// Replace any remaining '^' characters with spaces
|
||||
const cleaned = commaBetweenFirstAndLast.replace(/\^/g, ' ');
|
||||
|
||||
// Trim any extraneous whitespace
|
||||
return cleaned.trim();
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import makeCancelable from './makeCancelable';
|
||||
import hotkeys from './hotkeys';
|
||||
import Queue from './Queue';
|
||||
import isDicomUid from './isDicomUid';
|
||||
import formatPN from './formatPN';
|
||||
import resolveObjectPath from './resolveObjectPath';
|
||||
import * as hierarchicalListUtils from './hierarchicalListUtils';
|
||||
import * as progressTrackingUtils from './progressTrackingUtils';
|
||||
@@ -26,6 +27,7 @@ const utils = {
|
||||
addServers,
|
||||
sortBy,
|
||||
writeScript,
|
||||
formatPN,
|
||||
b64toBlob,
|
||||
StackManager,
|
||||
studyMetadataManager,
|
||||
|
||||
@@ -33,6 +33,8 @@ export {
|
||||
Dialog,
|
||||
Dropdown,
|
||||
EmptyStudies,
|
||||
ExpandableToolbarButton,
|
||||
ListMenu,
|
||||
Icon,
|
||||
IconButton,
|
||||
Input,
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="19" height="19" viewBox="0 0 19 19">
|
||||
<g fill="currentColor" fill-rule="evenodd">
|
||||
<g stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5">
|
||||
<path d="M.188.187L8.813 8.812M8.813.187L.188 8.812" transform="translate(5 5)"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 355 B |
@@ -3,12 +3,13 @@ import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import { Icon, Typography } from '@ohif/ui';
|
||||
|
||||
const EmptyStudies = ({ className }) => {
|
||||
// TODO: Add loading spinner to OHIF + use it here.
|
||||
const EmptyStudies = ({ className, isLoading }) => {
|
||||
return (
|
||||
<div className={classnames('flex-col inline-flex items-center', className)}>
|
||||
<Icon name="magnifier" className="mb-4" />
|
||||
<Typography className="text-primary-light" variant="h5">
|
||||
No studies available
|
||||
{!isLoading ? 'No studies available' : 'Loading...'}
|
||||
</Typography>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
.ExpandableToolbarButton:hover .ExpandableToolbarButton__arrow:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
bottom: -10px;
|
||||
border-width: 10px 10px 0;
|
||||
border-style: solid;
|
||||
border-color:#5acce6 transparent;
|
||||
}
|
||||
|
||||
.ExpandableToolbarButton .ExpandableToolbarButton__content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.ExpandableToolbarButton:hover .ExpandableToolbarButton__content {
|
||||
display: block;
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
|
||||
import { IconButton, Icon } from '@ohif/ui';
|
||||
|
||||
import './ExpandableToolbarButton.css';
|
||||
|
||||
const ExpandableToolbarButton = ({
|
||||
type,
|
||||
id,
|
||||
isActive,
|
||||
onClick,
|
||||
icon,
|
||||
className,
|
||||
content: Content,
|
||||
contentProps
|
||||
}) => {
|
||||
const classes = {
|
||||
type: {
|
||||
primary: isActive
|
||||
? 'text-black'
|
||||
: 'text-common-bright hover:bg-primary-dark hover:text-primary-light',
|
||||
secondary: isActive
|
||||
? 'text-black'
|
||||
: 'text-white hover:bg-secondary-dark focus:bg-secondary-dark',
|
||||
},
|
||||
};
|
||||
|
||||
const onChildClickHandler = (...args) => {
|
||||
onClick(...args);
|
||||
|
||||
if (contentProps.onClick) {
|
||||
contentProps.onClick(...args);
|
||||
}
|
||||
};
|
||||
|
||||
const onClickHandler = (...args) => {
|
||||
onClick(...args);
|
||||
};
|
||||
|
||||
return (
|
||||
<div key={id} className="ExpandableToolbarButton">
|
||||
<IconButton
|
||||
variant={isActive ? 'contained' : 'text'}
|
||||
className={classnames(
|
||||
'mx-1',
|
||||
classes.type[type],
|
||||
isActive && 'ExpandableToolbarButton__arrow'
|
||||
)}
|
||||
onClick={onClickHandler}
|
||||
key={id}
|
||||
>
|
||||
<Icon name={icon} />
|
||||
</IconButton>
|
||||
<div className="absolute z-10 pt-4">
|
||||
<div className={classnames("ExpandableToolbarButton__content w-48", className)}>
|
||||
<Content {...contentProps} onClick={onChildClickHandler} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const noop = () => { };
|
||||
|
||||
ExpandableToolbarButton.defaultProps = {
|
||||
isActive: false,
|
||||
type: 'primary',
|
||||
content: null,
|
||||
onClick: noop,
|
||||
};
|
||||
|
||||
ExpandableToolbarButton.propTypes = {
|
||||
/* Influences background/hover styling */
|
||||
type: PropTypes.oneOf(['primary', 'secondary']),
|
||||
id: PropTypes.string.isRequired,
|
||||
isActive: PropTypes.bool,
|
||||
onClick: PropTypes.func.isRequired,
|
||||
icon: PropTypes.string.isRequired,
|
||||
/** Expandable toolbar button content can be replaced for a customized content by passing a node to this value. */
|
||||
content: PropTypes.oneOfType([PropTypes.node, PropTypes.func]),
|
||||
contentProps: PropTypes.object,
|
||||
};
|
||||
|
||||
export default ExpandableToolbarButton;
|
||||
@@ -0,0 +1,58 @@
|
||||
---
|
||||
name: Expandable Toolbar Button
|
||||
menu: General
|
||||
route: components/expandableToolbarButton
|
||||
---
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Playground, Props } from 'docz';
|
||||
import { ExpandableToolbarButton, ListMenu } from '@ohif/ui';
|
||||
|
||||
# Toolbar Button
|
||||
|
||||
Expandable Toolbar Buttons are used to populate the Toolbar.
|
||||
|
||||
## Import
|
||||
|
||||
```javascript
|
||||
import { ExpandableToolbarButton, ListMenu } from '@ohif/ui';
|
||||
```
|
||||
|
||||
<Playground>
|
||||
{() => {
|
||||
const props = {
|
||||
content: ListMenu,
|
||||
contentProps: {
|
||||
options: [
|
||||
{ value: 'windowLevelPreset1', title: 'Soft tissue', subtitle: '400 / 40' },
|
||||
{ value: 'windowLevelPreset2', title: 'Lung', subtitle: '1500 / -600' },
|
||||
{ value: 'windowLevelPreset3', title: 'Liver', subtitle: '150 / 90' },
|
||||
{ value: 'windowLevelPreset4', title: 'Bone', subtitle: '80 / 40' },
|
||||
{ value: 'windowLevelPreset5', title: 'Brain', subtitle: '2500 / 480' },
|
||||
],
|
||||
renderer: ({ title, subtitle, isActive, index }) => (
|
||||
<>
|
||||
<div>
|
||||
<span className={classnames(isActive ? "text-black" : "text-white", "mr-2 text-base")}>
|
||||
{title}
|
||||
</span>
|
||||
<span className={classnames(isActive ? "text-black" : "text-aqua-pale", "font-thin text-sm")}>
|
||||
{subtitle}
|
||||
</span>
|
||||
</div>
|
||||
<span className={classnames(isActive ? "text-black" : "text-primary-active", "text-sm")}>{index + 1}</span>
|
||||
</>
|
||||
)
|
||||
}
|
||||
};
|
||||
return (
|
||||
<div className="py-10 flex justify-center">
|
||||
<ExpandableToolbarButton {...props} />
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</Playground>
|
||||
|
||||
## Properties
|
||||
|
||||
<Props of={ToolbarButton} />
|
||||
@@ -0,0 +1,2 @@
|
||||
import ExpandableToolbarButton from './ExpandableToolbarButton';
|
||||
export default ExpandableToolbarButton;
|
||||
@@ -4,6 +4,7 @@ import React from 'react';
|
||||
import arrowDown from './../../assets/icons/arrow-down.svg';
|
||||
import calendar from './../../assets/icons/calendar.svg';
|
||||
import cancel from './../../assets/icons/cancel.svg';
|
||||
import close from './../../assets/icons/close.svg';
|
||||
import dottedCircle from './../../assets/icons/dotted-circle.svg';
|
||||
import circledCheckmark from './../../assets/icons/circled-checkmark.svg';
|
||||
import chevronDown from './../../assets/icons/chevron-down.svg';
|
||||
@@ -49,6 +50,7 @@ const ICONS = {
|
||||
'arrow-down': arrowDown,
|
||||
calendar: calendar,
|
||||
cancel: cancel,
|
||||
close: close,
|
||||
'dotted-circle': dottedCircle,
|
||||
'circled-checkmark': circledCheckmark,
|
||||
'chevron-down': chevronDown,
|
||||
|
||||
@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
|
||||
const baseClasses =
|
||||
'text-center items-center justify-center outline-none transition duration-300 ease-in-out font-bold focus:outline-none';
|
||||
'text-center items-center justify-center transition duration-300 ease-in-out outline-none font-bold focus:outline-none';
|
||||
|
||||
const roundedClasses = {
|
||||
none: '',
|
||||
@@ -84,7 +84,7 @@ const IconButton = ({
|
||||
}) => {
|
||||
const buttonElement = useRef(null);
|
||||
|
||||
const handleOnClick = (e) => {
|
||||
const handleOnClick = e => {
|
||||
buttonElement.current.blur();
|
||||
onClick(e);
|
||||
};
|
||||
|
||||
@@ -4,11 +4,11 @@ import Label from '../Label';
|
||||
import classnames from 'classnames';
|
||||
|
||||
const baseInputClasses =
|
||||
'shadow transition duration-300 appearance-none border rounded w-full py-2 px-3 text-sm text-white hover:border-gray-500 leading-tight focus:border-gray-500 focus:outline-none';
|
||||
'shadow transition duration-300 appearance-none border border-primary-main hover:border-gray-500 focus:border-gray-500 focus:outline-none rounded w-full py-2 px-3 mt-2 text-sm text-white leading-tight focus:outline-none';
|
||||
|
||||
const transparentClasses = {
|
||||
true: 'bg-transparent',
|
||||
false: '',
|
||||
false: 'bg-black',
|
||||
};
|
||||
|
||||
const Input = ({
|
||||
@@ -16,7 +16,7 @@ const Input = ({
|
||||
containerClassName = '',
|
||||
labelClassName = '',
|
||||
className = '',
|
||||
transparent = true,
|
||||
transparent = false,
|
||||
type = 'text',
|
||||
value,
|
||||
onChange,
|
||||
|
||||
@@ -43,7 +43,7 @@ InputText.propTypes = {
|
||||
isSortable: PropTypes.bool,
|
||||
sortDirection: PropTypes.oneOf(['ascending', 'descending', 'none']),
|
||||
onLabelClick: PropTypes.func,
|
||||
value: PropTypes.string,
|
||||
value: PropTypes.any,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import React, { useState } from 'react';
|
||||
import classnames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const ListMenu = ({ options = [], renderer, onClick }) => {
|
||||
const [selectedIndex, setSelectedIndex] = useState(null);
|
||||
|
||||
const ListItem = (props) => {
|
||||
const flex = 'flex flex-row justify-between items-center';
|
||||
const theme = 'bg-indigo-dark';
|
||||
const hover = 'hover:bg-primary-dark';
|
||||
const spacing = 'p-3 h-8';
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classnames(
|
||||
flex,
|
||||
theme,
|
||||
spacing,
|
||||
'cursor-pointer',
|
||||
!props.isActive && hover,
|
||||
props.isActive && 'bg-primary-light',
|
||||
)}
|
||||
onClick={props.onClick}
|
||||
>
|
||||
{renderer && renderer(props)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col rounded-md bg-secondary-dark pt-2 pb-2">
|
||||
{options.map((option, index) => {
|
||||
const onClickHandler = () => {
|
||||
setSelectedIndex(index);
|
||||
onClick({ ...option, index });
|
||||
};
|
||||
|
||||
return (
|
||||
<ListItem
|
||||
key={`ListItem${index}`}
|
||||
{...option}
|
||||
index={index}
|
||||
isActive={selectedIndex === index}
|
||||
onClick={onClickHandler}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const noop = () => { };
|
||||
|
||||
ListMenu.propTypes = {
|
||||
options: PropTypes.array.isRequired,
|
||||
renderer: PropTypes.func.isRequired,
|
||||
onClick: PropTypes.func
|
||||
};
|
||||
|
||||
ListMenu.defaultProps = {
|
||||
onClick: noop
|
||||
};
|
||||
|
||||
export default ListMenu;
|
||||
@@ -0,0 +1,52 @@
|
||||
---
|
||||
name: List Menu
|
||||
menu: General
|
||||
route: components/listMenu
|
||||
---
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Playground, Props } from 'docz';
|
||||
import { ListMenu } from '@ohif/ui';
|
||||
|
||||
# List Menu
|
||||
|
||||
List Menus are used to populate expandable Toolbar.
|
||||
|
||||
## Import
|
||||
|
||||
```javascript
|
||||
import { ListMenu } from '@ohif/ui';
|
||||
```
|
||||
|
||||
<Playground>
|
||||
{() => {
|
||||
return (
|
||||
<ToolbarButton options={[
|
||||
{ value: 'windowLevelPreset1', title: 'Soft tissue', subtitle: '400 / 40' },
|
||||
{ value: 'windowLevelPreset2', title: 'Lung', subtitle: '1500 / -600' },
|
||||
{ value: 'windowLevelPreset3', title: 'Liver', subtitle: '150 / 90' },
|
||||
{ value: 'windowLevelPreset4', title: 'Bone', subtitle: '80 / 40' },
|
||||
{ value: 'windowLevelPreset5', title: 'Brain', subtitle: '2500 / 480' },
|
||||
]}
|
||||
renderer={
|
||||
({ title, subtitle, isActive, index }) => (
|
||||
<>
|
||||
<div>
|
||||
<span className={classnames(isActive ? "text-black" : "text-white", "mr-2 text-base")}>
|
||||
{title}
|
||||
</span>
|
||||
<span className={classnames(isActive ? "text-black" : "text-aqua-pale", "font-thin text-sm")}>
|
||||
{subtitle}
|
||||
</span>
|
||||
</div>
|
||||
<span className={classnames(isActive ? "text-black" : "text-primary-active", "text-sm")}>{index + 1}</span>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
</Playground>
|
||||
|
||||
## Properties
|
||||
|
||||
<Props of={ToolbarButton} />
|
||||
@@ -0,0 +1,2 @@
|
||||
import ListMenu from './ListMenu';
|
||||
export default ListMenu;
|
||||
@@ -14,7 +14,7 @@ const MeasurementTable = ({ data, title, amount, onClick, onEdit }) => {
|
||||
</div>
|
||||
<div className="overflow-y-auto overflow-x-hidden ohif-scrollbar max-h-112">
|
||||
{!!data.length &&
|
||||
data.map((measurementItem) => {
|
||||
data.map(measurementItem => {
|
||||
const { id, label, displayText, isActive } = measurementItem;
|
||||
return (
|
||||
<div
|
||||
@@ -45,9 +45,11 @@ const MeasurementTable = ({ data, title, amount, onClick, onEdit }) => {
|
||||
<span className="text-base text-primary-light mb-1">
|
||||
{label}
|
||||
</span>
|
||||
<span className="pl-2 border-l border-primary-light text-base text-white">
|
||||
{displayText}
|
||||
</span>
|
||||
{displayText.map(line => (
|
||||
<span className="pl-2 border-l border-primary-light text-base text-white">
|
||||
{line}
|
||||
</span>
|
||||
))}
|
||||
<Icon
|
||||
className={classnames(
|
||||
'text-white w-4 absolute cursor-pointer transition duration-300',
|
||||
@@ -61,7 +63,7 @@ const MeasurementTable = ({ data, title, amount, onClick, onEdit }) => {
|
||||
right: 4,
|
||||
transform: isActive ? '' : 'translateX(100%)',
|
||||
}}
|
||||
onClick={(e) => {
|
||||
onClick={e => {
|
||||
// stopPropagation needed to avoid disable the current active item
|
||||
e.stopPropagation();
|
||||
onEdit(id);
|
||||
@@ -108,7 +110,7 @@ MeasurementTable.propTypes = {
|
||||
PropTypes.shape({
|
||||
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
label: PropTypes.string,
|
||||
displayText: PropTypes.string,
|
||||
displayText: PropTypes.arrayOf(PropTypes.string),
|
||||
isActive: PropTypes.bool,
|
||||
})
|
||||
),
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
.modal-content {
|
||||
max-height: calc(100vh - theme('spacing.250px'));
|
||||
}
|
||||
@@ -1,25 +1,14 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ReactModal from 'react-modal';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { Typography, useModal } from '@ohif/ui';
|
||||
import './Modal.css';
|
||||
|
||||
const customStyle = {
|
||||
overlay: {
|
||||
zIndex: 1071,
|
||||
backgroundColor: 'rgb(0, 0, 0, 0.8)',
|
||||
display: 'flex',
|
||||
alignItems: 'flex-start',
|
||||
justifyContent: 'center',
|
||||
padding: '40px 0',
|
||||
},
|
||||
};
|
||||
import { Typography, useModal, IconButton, Icon } from '@ohif/ui';
|
||||
|
||||
ReactModal.setAppElement(document.getElementById('root'));
|
||||
|
||||
const Modal = ({
|
||||
className,
|
||||
closeButton,
|
||||
shouldCloseOnEsc,
|
||||
isOpen,
|
||||
@@ -39,13 +28,15 @@ const Modal = ({
|
||||
<header className="mb-6 pb-4 border-b border-secondary-main">
|
||||
<Typography variant="h4">{title}</Typography>
|
||||
{closeButton && (
|
||||
<button
|
||||
className="absolute top-0 right-0 bg-primary-main focus:outline-none text-white rounded-full text-2xl w-8 h-8 flex justify-center items-center -mr-3 -mt-3"
|
||||
<IconButton
|
||||
className="absolute top-0 right-0 focus:outline-none flex -mr-3 -mt-3"
|
||||
data-cy="close-button"
|
||||
color="primary"
|
||||
onClick={onClose}
|
||||
rounded="full"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
<Icon name="close" className="text-white w-8 h-8" />
|
||||
</IconButton>
|
||||
)}
|
||||
</header>
|
||||
)
|
||||
@@ -54,24 +45,17 @@ const Modal = ({
|
||||
|
||||
return (
|
||||
<ReactModal
|
||||
className={classNames(
|
||||
`relative py-6 w-11/12 lg:w-10/12 xl:w-1/2 max-h-full outline-none bg-primary-dark border border-secondary-main text-white rounded ${className}`
|
||||
)}
|
||||
className="relative py-6 w-11/12 lg:w-10/12 xl:w-1/2 max-h-full outline-none bg-primary-dark border border-secondary-main text-white rounded"
|
||||
overlayClassName="fixed top-0 left-0 right-0 bottom-0 z-50 bg-overlay flex items-start justify-center py-16"
|
||||
shouldCloseOnEsc={shouldCloseOnEsc}
|
||||
onRequestClose={handleClose}
|
||||
isOpen={isOpen}
|
||||
title={title}
|
||||
style={customStyle}
|
||||
>
|
||||
<>
|
||||
<div className="px-6">{renderHeader()}</div>
|
||||
<section
|
||||
className="ohif-scrollbar overflow-y-auto px-6"
|
||||
style={{ maxHeight: 'calc(100vh - 200px)' }}
|
||||
>
|
||||
{children}
|
||||
</section>
|
||||
</>
|
||||
<div className="px-6">{renderHeader()}</div>
|
||||
<section className="ohif-scrollbar modal-content overflow-y-auto px-6">
|
||||
{children}
|
||||
</section>
|
||||
</ReactModal>
|
||||
);
|
||||
};
|
||||
@@ -81,7 +65,6 @@ Modal.defaultProps = {
|
||||
};
|
||||
|
||||
Modal.propTypes = {
|
||||
className: PropTypes.string,
|
||||
closeButton: PropTypes.bool,
|
||||
shouldCloseOnEsc: PropTypes.bool,
|
||||
isOpen: PropTypes.bool,
|
||||
|
||||
@@ -76,15 +76,9 @@ const Select = ({
|
||||
options={options}
|
||||
value={selectedOptions}
|
||||
onChange={(selectedOptions, { action }) => {
|
||||
if (!isMulti) {
|
||||
onChange(selectedOptions, action);
|
||||
return;
|
||||
}
|
||||
|
||||
const newSelection = selectedOptions.reduce(
|
||||
(acc, curr) => acc.concat([curr.value]),
|
||||
[]
|
||||
);
|
||||
const newSelection = !selectedOptions.length
|
||||
? selectedOptions
|
||||
: selectedOptions.reduce((acc, curr) => acc.concat([curr.value]), []);
|
||||
onChange(newSelection, action);
|
||||
}}
|
||||
></ReactSelect>
|
||||
|
||||
@@ -48,6 +48,7 @@ const StudyListFilter = ({
|
||||
color="inherit"
|
||||
className="text-primary-active"
|
||||
startIcon={<Icon name="info-link" className="w-2" />}
|
||||
onClick={showLearnMoreContent}
|
||||
>
|
||||
<span className="flex flex-col flex-1">
|
||||
<span onClick={showLearnMoreContent}>Learn more</span>
|
||||
|
||||
@@ -25,6 +25,7 @@ const ToolbarButton = ({
|
||||
};
|
||||
|
||||
const shouldShowDropdown = !!isActive && !!dropdownContent;
|
||||
|
||||
return (
|
||||
<div key={id}>
|
||||
<Tooltip
|
||||
|
||||
@@ -24,7 +24,7 @@ const arrowPositionStyle = {
|
||||
},
|
||||
};
|
||||
|
||||
const Tooltip = ({ content, isSticky, position, tight, children }) => {
|
||||
const Tooltip = ({ content, isSticky, position, tight, children, isDisabled }) => {
|
||||
const [isActive, setIsActive] = useState(false);
|
||||
|
||||
const handleMouseOver = () => {
|
||||
@@ -39,7 +39,7 @@ const Tooltip = ({ content, isSticky, position, tight, children }) => {
|
||||
}
|
||||
};
|
||||
|
||||
const isOpen = isSticky || isActive;
|
||||
const isOpen = (isSticky || isActive) && !isDisabled;
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -84,10 +84,12 @@ Tooltip.defaultProps = {
|
||||
tight: false,
|
||||
isSticky: false,
|
||||
position: 'bottom',
|
||||
isDisabled: false
|
||||
};
|
||||
|
||||
Tooltip.propTypes = {
|
||||
// Allow null
|
||||
/** prevents tooltip from rendering despite hover/active/sticky */
|
||||
isDisabled: PropTypes.bool,
|
||||
content: PropTypes.oneOfType([PropTypes.node, PropTypes.func]),
|
||||
position: PropTypes.oneOf([
|
||||
'bottom',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import { Icon, ButtonGroup, Button, Tooltip } from '@ohif/ui';
|
||||
@@ -13,9 +13,11 @@ const classes = {
|
||||
const ViewportActionBar = ({
|
||||
studyData,
|
||||
showNavArrows,
|
||||
showPatientInfo,
|
||||
showPatientInfo: patientInfoVisibility,
|
||||
onSeriesChange,
|
||||
}) => {
|
||||
const [showPatientInfo, setShowPatientInfo] = useState(patientInfoVisibility);
|
||||
|
||||
const {
|
||||
label,
|
||||
isTracked,
|
||||
@@ -37,6 +39,8 @@ const ViewportActionBar = ({
|
||||
scanner,
|
||||
} = patientInformation;
|
||||
|
||||
const onPatientInfoClick = () => setShowPatientInfo(!showPatientInfo)
|
||||
|
||||
const renderIconStatus = () => {
|
||||
if (modality === 'SR') {
|
||||
return (
|
||||
@@ -60,29 +64,30 @@ const ViewportActionBar = ({
|
||||
{!isTracked ? (
|
||||
<Icon name="dotted-circle" className="w-6 text-primary-light" />
|
||||
) : (
|
||||
<Tooltip
|
||||
position="bottom-left"
|
||||
content={
|
||||
<div className="flex py-2">
|
||||
<div className="flex pt-1">
|
||||
<Icon name="info-link" className="w-4 text-primary-main" />
|
||||
</div>
|
||||
<div className="flex ml-4">
|
||||
<span className="text-base text-common-light">
|
||||
Series is
|
||||
<Tooltip
|
||||
position="bottom-left"
|
||||
content={
|
||||
<div className="flex py-2">
|
||||
<div className="flex pt-1">
|
||||
<Icon name="info-link" className="w-4 text-primary-main" />
|
||||
</div>
|
||||
<div className="flex ml-4">
|
||||
<span className="text-base text-common-light">
|
||||
Series is
|
||||
<span className="font-bold text-white"> tracked</span> and
|
||||
can be viewed <br /> in the measurement panel
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Icon name="tracked" className="w-6 text-primary-light" />
|
||||
</Tooltip>
|
||||
)}
|
||||
}
|
||||
>
|
||||
<Icon name="tracked" className="w-6 text-primary-light" />
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center p-2 border-b border-primary-light">
|
||||
<div className="flex flex-grow">
|
||||
@@ -131,19 +136,18 @@ const ViewportActionBar = ({
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
)}
|
||||
{showPatientInfo && (
|
||||
<div className="flex ml-4 mr-2">
|
||||
<PatientInfo
|
||||
patientName={patientName}
|
||||
patientSex={patientSex}
|
||||
patientAge={patientAge}
|
||||
MRN={MRN}
|
||||
thickness={thickness}
|
||||
spacing={spacing}
|
||||
scanner={scanner}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex ml-4 mr-2 cursor-pointer" onClick={onPatientInfoClick}>
|
||||
<PatientInfo
|
||||
isOpen={showPatientInfo}
|
||||
patientName={patientName}
|
||||
patientSex={patientSex}
|
||||
patientAge={patientAge}
|
||||
MRN={MRN}
|
||||
thickness={thickness}
|
||||
spacing={spacing}
|
||||
scanner={scanner}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -174,7 +178,7 @@ ViewportActionBar.propTypes = {
|
||||
|
||||
ViewportActionBar.defaultProps = {
|
||||
showNavArrows: true,
|
||||
showPatientInfo: true,
|
||||
showPatientInfo: false,
|
||||
};
|
||||
|
||||
function PatientInfo({
|
||||
@@ -185,11 +189,14 @@ function PatientInfo({
|
||||
thickness,
|
||||
spacing,
|
||||
scanner,
|
||||
isOpen,
|
||||
}) {
|
||||
return (
|
||||
<Tooltip
|
||||
isSticky
|
||||
isDisabled={!isOpen}
|
||||
position="bottom-right"
|
||||
content={
|
||||
content={isOpen && (
|
||||
<div className="flex py-2">
|
||||
<div className="flex pt-1">
|
||||
<Icon name="info-link" className="w-4 text-primary-main" />
|
||||
@@ -236,7 +243,7 @@ function PatientInfo({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
)}
|
||||
>
|
||||
<div className="relative flex justify-end">
|
||||
<div className="relative">
|
||||
|
||||
@@ -6,10 +6,11 @@ import React, {
|
||||
useRef,
|
||||
} from 'react';
|
||||
|
||||
import classnames from 'classnames';
|
||||
|
||||
import {
|
||||
Typography,
|
||||
InputText,
|
||||
InputNumber,
|
||||
Input,
|
||||
Tooltip,
|
||||
IconButton,
|
||||
Icon,
|
||||
@@ -169,7 +170,10 @@ const ViewportDownloadForm = ({
|
||||
);
|
||||
};
|
||||
|
||||
const validSize = value => (value >= minimumSize ? value : minimumSize);
|
||||
const validSize = useCallback(
|
||||
value => (value >= minimumSize ? value : minimumSize),
|
||||
[minimumSize]
|
||||
);
|
||||
|
||||
const loadAndUpdateViewports = useCallback(async () => {
|
||||
const { width: scaledWidth, height: scaledHeight } = await loadImage(
|
||||
@@ -274,7 +278,7 @@ const ViewportDownloadForm = ({
|
||||
|
||||
<div className="mt-6 flex flex-col">
|
||||
<div className="w-full mb-4">
|
||||
<InputText
|
||||
<Input
|
||||
data-cy="file-name"
|
||||
value={filename}
|
||||
onChange={value => setFilename(value)}
|
||||
@@ -286,7 +290,8 @@ const ViewportDownloadForm = ({
|
||||
<div className="flex w-1/3">
|
||||
<div className="flex flex-col flex-grow">
|
||||
<div className="w-full">
|
||||
<InputNumber
|
||||
<Input
|
||||
type="number"
|
||||
min={minimumSize}
|
||||
max={maximumSize}
|
||||
label="Image width (px)"
|
||||
@@ -297,7 +302,8 @@ const ViewportDownloadForm = ({
|
||||
{renderErrorHandler('width')}
|
||||
</div>
|
||||
<div className="w-full mt-4">
|
||||
<InputNumber
|
||||
<Input
|
||||
type="number"
|
||||
min={minimumSize}
|
||||
max={maximumSize}
|
||||
label="Image height (px)"
|
||||
@@ -366,20 +372,18 @@ const ViewportDownloadForm = ({
|
||||
|
||||
<div className="mt-8">
|
||||
<div
|
||||
className="hidden"
|
||||
style={{
|
||||
height: viewportElementDimensions.height,
|
||||
width: viewportElementDimensions.width,
|
||||
position: 'absolute',
|
||||
left: '9999px',
|
||||
}}
|
||||
ref={ref => setViewportElement(ref)}
|
||||
>
|
||||
<canvas
|
||||
className={canvasClass}
|
||||
className={classnames('block', canvasClass)}
|
||||
style={{
|
||||
height: downloadCanvas.height,
|
||||
width: downloadCanvas.width,
|
||||
display: 'block',
|
||||
}}
|
||||
width={downloadCanvas.width}
|
||||
height={downloadCanvas.height}
|
||||
|
||||
@@ -41,6 +41,8 @@ import ThumbnailNoImage from './ThumbnailNoImage';
|
||||
import ThumbnailTracked from './ThumbnailTracked';
|
||||
import ThumbnailList from './ThumbnailList';
|
||||
import ToolbarButton from './ToolbarButton';
|
||||
import ExpandableToolbarButton from './ExpandableToolbarButton';
|
||||
import ListMenu from './ListMenu';
|
||||
import Tooltip from './Tooltip';
|
||||
import Typography from './Typography';
|
||||
import Viewport from './Viewport';
|
||||
@@ -56,6 +58,8 @@ export {
|
||||
Dialog,
|
||||
Dropdown,
|
||||
EmptyStudies,
|
||||
ExpandableToolbarButton,
|
||||
ListMenu,
|
||||
Icon,
|
||||
IconButton,
|
||||
Input,
|
||||
|
||||
@@ -10,12 +10,20 @@ module.exports = {
|
||||
xl: '1280px',
|
||||
},
|
||||
colors: {
|
||||
overlay: 'rgba(0, 0, 0, 0.8)',
|
||||
transparent: 'transparent',
|
||||
black: '#000',
|
||||
white: '#fff',
|
||||
initial: 'initial',
|
||||
inherit: 'inherit',
|
||||
|
||||
indigo: {
|
||||
dark: '#0b1a42',
|
||||
},
|
||||
aqua: {
|
||||
pale: '#7bb2ce',
|
||||
},
|
||||
|
||||
primary: {
|
||||
light: '#5acce6',
|
||||
main: '#0944b3',
|
||||
|
||||
@@ -67,9 +67,9 @@
|
||||
"classnames": "^2.2.6",
|
||||
"core-js": "^3.2.1",
|
||||
"cornerstone-math": "^0.1.8",
|
||||
"cornerstone-tools": "4.15.1",
|
||||
"cornerstone-tools": "4.16.0",
|
||||
"cornerstone-wado-image-loader": "^3.1.2",
|
||||
"dcmjs": "^0.12.2",
|
||||
"dcmjs": "0.14.0",
|
||||
"dicom-parser": "^1.8.3",
|
||||
"dicomweb-client": "^0.4.4",
|
||||
"dotenv-webpack": "^1.7.0",
|
||||
|
||||
@@ -19,7 +19,9 @@ window.config = {
|
||||
imageRendering: 'wadors',
|
||||
thumbnailRendering: 'wadors',
|
||||
enableStudyLazyLoad: true,
|
||||
supportsFuzzyMatching: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
defaultDataSourceName: 'dicomweb',
|
||||
};
|
||||
@@ -1,10 +1,12 @@
|
||||
/**
|
||||
* CSS Grid Reference: http://grid.malven.co/
|
||||
*/
|
||||
import React from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { ViewportGrid, ViewportPane, useViewportGrid } from '@ohif/ui';
|
||||
import EmptyViewport from './EmptyViewport';
|
||||
import { classes } from '@ohif/core';
|
||||
const { ImageSet } = classes;
|
||||
|
||||
function ViewerViewportGrid(props) {
|
||||
const { servicesManager, viewportComponents, dataSource } = props;
|
||||
@@ -20,6 +22,29 @@ function ViewerViewportGrid(props) {
|
||||
// TODO -> Need some way of selecting which displaySets hit the viewports.
|
||||
const { DisplaySetService } = servicesManager.services;
|
||||
|
||||
useEffect(() => {
|
||||
const { unsubscribe } = DisplaySetService.subscribe(
|
||||
DisplaySetService.EVENTS.DISPLAY_SETS_CHANGED,
|
||||
displaySets => {
|
||||
displaySets.sort((a, b) => {
|
||||
const isImageSet = x => x instanceof ImageSet;
|
||||
return (isImageSet(a) === isImageSet(b)) ? 0 : isImageSet(a) ? -1 : 1;
|
||||
});
|
||||
dispatch({
|
||||
type: 'SET_DISPLAYSET_FOR_VIEWPORT',
|
||||
payload: {
|
||||
viewportIndex: 0,
|
||||
displaySetInstanceUID: displaySets[0].displaySetInstanceUID,
|
||||
},
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
return () => {
|
||||
unsubscribe();
|
||||
};
|
||||
}, []);
|
||||
|
||||
// TODO -> Make a HangingProtocolService
|
||||
const HangingProtocolService = displaySets => {
|
||||
let displaySetInstanceUID;
|
||||
|
||||
@@ -46,14 +46,17 @@ function DataSourceWrapper(props) {
|
||||
// studies.processResults --> <LayoutTemplate studies={} />
|
||||
// But only for LayoutTemplate type of 'list'?
|
||||
// Or no data fetching here, and just hand down my source
|
||||
const [data, setData] = useState();
|
||||
const [data, setData] = useState([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
useEffect(() => {
|
||||
// 204: no content
|
||||
async function getData() {
|
||||
setIsLoading(true);
|
||||
const searchResults = await dataSource.query.studies.search(
|
||||
queryFilterValues
|
||||
);
|
||||
setData(searchResults);
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -61,23 +64,19 @@ function DataSourceWrapper(props) {
|
||||
} catch (ex) {
|
||||
console.warn(ex);
|
||||
}
|
||||
console.log('DataSourceWrapper: useEffect');
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [history.location.search]);
|
||||
// queryFilterValues
|
||||
|
||||
// TODO: Better way to pass DataSource?
|
||||
return (
|
||||
<React.Fragment>
|
||||
{data && (
|
||||
<LayoutTemplate
|
||||
{...rest}
|
||||
history={history}
|
||||
data={data}
|
||||
dataSource={dataSource}
|
||||
/>
|
||||
)}
|
||||
</React.Fragment>
|
||||
<LayoutTemplate
|
||||
{...rest}
|
||||
history={history}
|
||||
data={data}
|
||||
dataSource={dataSource}
|
||||
isLoadingData={isLoading}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React, { Suspense, useState, useEffect } from 'react';
|
||||
import classnames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Link } from 'react-router-dom';
|
||||
@@ -29,15 +29,16 @@ const seriesInStudiesMap = new Map();
|
||||
* TODO:
|
||||
* - debounce `setFilterValues` (150ms?)
|
||||
*/
|
||||
function WorkList({ history, data: studies, dataSource }) {
|
||||
function WorkList({ history, data: studies, isLoadingData, dataSource }) {
|
||||
// ~ Modes
|
||||
const [appConfig] = useAppConfig();
|
||||
// ~ Filters
|
||||
const query = useQuery();
|
||||
const queryFilterValues = _getQueryFilterValues(query);
|
||||
const [filterValues, _setFilterValues] = useState(
|
||||
Object.assign({}, defaultFilterValues, queryFilterValues)
|
||||
);
|
||||
const [filterValues, _setFilterValues] = useState({
|
||||
...defaultFilterValues,
|
||||
...queryFilterValues,
|
||||
});
|
||||
const debouncedFilterValues = useDebounce(filterValues, 200);
|
||||
const { resultsPerPage, pageNumber, sortBy, sortDirection } = filterValues;
|
||||
|
||||
@@ -81,6 +82,7 @@ function WorkList({ history, data: studies, dataSource }) {
|
||||
|
||||
return 0;
|
||||
});
|
||||
|
||||
// ~ Rows & Studies
|
||||
const [expandedRows, setExpandedRows] = useState([]);
|
||||
const [studiesWithSeriesData, setStudiesWithSeriesData] = useState([]);
|
||||
@@ -191,6 +193,7 @@ function WorkList({ history, data: studies, dataSource }) {
|
||||
return filterValues[name] !== defaultFilterValues[name];
|
||||
});
|
||||
};
|
||||
|
||||
const tableDataSource = sortedStudies.map((study, key) => {
|
||||
const rowKey = key + 1;
|
||||
const isExpanded = expandedRows.some(k => k === rowKey);
|
||||
@@ -395,7 +398,7 @@ function WorkList({ history, data: studies, dataSource }) {
|
||||
</>
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center pt-48">
|
||||
<EmptyStudies />
|
||||
<EmptyStudies isLoading={isLoadingData} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user