Fixes after rebasing
This commit is contained in:
parent
70b7158c9b
commit
f3f50d11fe
@ -81,8 +81,12 @@ module.exports = (env, argv, { SRC_DIR, DIST_DIR }) => {
|
||||
'process.env.BUILD_NUM': JSON.stringify(BUILD_NUM),
|
||||
/* i18n */
|
||||
'process.env.USE_LOCIZE': JSON.stringify(process.env.USE_LOCIZE || ''),
|
||||
'process.env.LOCIZE_PROJECTID': JSON.stringify(process.env.LOCIZE_PROJECTID || ''),
|
||||
'process.env.LOCIZE_API_KEY': JSON.stringify(process.env.LOCIZE_API_KEY || ''),
|
||||
'process.env.LOCIZE_PROJECTID': JSON.stringify(
|
||||
process.env.LOCIZE_PROJECTID || ''
|
||||
),
|
||||
'process.env.LOCIZE_API_KEY': JSON.stringify(
|
||||
process.env.LOCIZE_API_KEY || ''
|
||||
),
|
||||
}),
|
||||
],
|
||||
// Fix: https://github.com/webpack-contrib/css-loader/issues/447#issuecomment-285598881
|
||||
|
||||
@ -180,7 +180,7 @@ class OHIFCornerstoneViewport extends Component {
|
||||
|
||||
if (
|
||||
displaySet.displaySetInstanceUID !==
|
||||
prevDisplaySet.displaySetInstanceUID ||
|
||||
prevDisplaySet.displaySetInstanceUID ||
|
||||
displaySet.SOPInstanceUID !== prevDisplaySet.SOPInstanceUID ||
|
||||
displaySet.frameIndex !== prevDisplaySet.frameIndex
|
||||
) {
|
||||
@ -216,18 +216,21 @@ class OHIFCornerstoneViewport extends Component {
|
||||
});
|
||||
}
|
||||
|
||||
const debouncedNewImageHandler = debounce(({ currentImageIdIndex, sopInstanceUid }) => {
|
||||
const { displaySet } = this.props.viewportData;
|
||||
const { StudyInstanceUID } = displaySet;
|
||||
if (currentImageIdIndex > 0) {
|
||||
this.props.onNewImage({
|
||||
StudyInstanceUID,
|
||||
SOPInstanceUID: sopInstanceUid,
|
||||
frameIndex: currentImageIdIndex,
|
||||
activeViewportIndex: viewportIndex,
|
||||
});
|
||||
}
|
||||
}, 700);
|
||||
const debouncedNewImageHandler = debounce(
|
||||
({ currentImageIdIndex, sopInstanceUid }) => {
|
||||
const { displaySet } = this.props.viewportData;
|
||||
const { StudyInstanceUID } = displaySet;
|
||||
if (currentImageIdIndex > 0) {
|
||||
this.props.onNewImage({
|
||||
StudyInstanceUID,
|
||||
SOPInstanceUID: sopInstanceUid,
|
||||
frameIndex: currentImageIdIndex,
|
||||
activeViewportIndex: viewportIndex,
|
||||
});
|
||||
}
|
||||
},
|
||||
700
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@ -36,7 +36,10 @@ describe('measurementServiceMappings.js', () => {
|
||||
};
|
||||
mappings = measurementServiceMappingsFactory(measurementServiceMock);
|
||||
handles = { start: { x: 1, y: 2 }, end: { x: 1, y: 2 } };
|
||||
points = [{ x: 1, y: 2 }, { x: 1, y: 2 }];
|
||||
points = [
|
||||
{ x: 1, y: 2 },
|
||||
{ x: 1, y: 2 },
|
||||
];
|
||||
csToolsAnnotation = {
|
||||
toolName: definition,
|
||||
measurementData: {
|
||||
|
||||
@ -8,9 +8,7 @@ import {
|
||||
import _downloadAndZip from './downloadAndZip';
|
||||
|
||||
const {
|
||||
utils: {
|
||||
Queue,
|
||||
},
|
||||
utils: { Queue },
|
||||
} = OHIF;
|
||||
|
||||
export function getCommands(context) {
|
||||
|
||||
@ -52,7 +52,10 @@ class DicomPDFViewport extends Component {
|
||||
|
||||
if (!this.props.useNative) {
|
||||
const pdf = await PDFJS.getDocument(fileURL).promise;
|
||||
this.setState(state => ({ ...state, pdf }), () => this.updatePDFCanvas());
|
||||
this.setState(
|
||||
state => ({ ...state, pdf }),
|
||||
() => this.updatePDFCanvas()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -14,7 +14,7 @@ const OHIFDicomRTStructSopClassHandler = {
|
||||
id: 'OHIFDicomRTStructSopClassHandler',
|
||||
type: MODULE_TYPES.SOP_CLASS_HANDLER,
|
||||
sopClassUIDs,
|
||||
getDisplaySetFromSeries: function (
|
||||
getDisplaySetFromSeries: function(
|
||||
series,
|
||||
study,
|
||||
dicomWebClient,
|
||||
@ -67,7 +67,7 @@ const OHIFDicomRTStructSopClassHandler = {
|
||||
}
|
||||
}
|
||||
|
||||
rtStructDisplaySet.load = function (referencedDisplaySet, studies) {
|
||||
rtStructDisplaySet.load = function(referencedDisplaySet, studies) {
|
||||
return loadRTStruct(
|
||||
rtStructDisplaySet,
|
||||
referencedDisplaySet,
|
||||
|
||||
@ -1,56 +1,262 @@
|
||||
import React from 'react';
|
||||
|
||||
const LoadingIndicator = ({ height = "200px", width = "200px", expand = false }) => {
|
||||
const LoadingIndicator = ({
|
||||
height = '200px',
|
||||
width = '200px',
|
||||
expand = false,
|
||||
}) => {
|
||||
return (
|
||||
<div style={expand ? { height: "100%", width: "100%" } : {}}>
|
||||
<svg style={{ margin: 'auto', background: 'none', display: 'block', shapeRendering: 'auto' }} width={width} height={height} viewBox="0 0 100 100" preserveAspectRatio="xMidYMid">
|
||||
<div style={expand ? { height: '100%', width: '100%' } : {}}>
|
||||
<svg
|
||||
style={{
|
||||
margin: 'auto',
|
||||
background: 'none',
|
||||
display: 'block',
|
||||
shapeRendering: 'auto',
|
||||
}}
|
||||
width={width}
|
||||
height={height}
|
||||
viewBox="0 0 100 100"
|
||||
preserveAspectRatio="xMidYMid"
|
||||
>
|
||||
<g transform="rotate(0 50 50)">
|
||||
<rect x="49" y="24" rx="0" ry="0" width="2" height="12" fill="#20a5d6">
|
||||
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.9166666666666666s" repeatCount="indefinite"></animate>
|
||||
<rect
|
||||
x="49"
|
||||
y="24"
|
||||
rx="0"
|
||||
ry="0"
|
||||
width="2"
|
||||
height="12"
|
||||
fill="#20a5d6"
|
||||
>
|
||||
<animate
|
||||
attributeName="opacity"
|
||||
values="1;0"
|
||||
keyTimes="0;1"
|
||||
dur="1s"
|
||||
begin="-0.9166666666666666s"
|
||||
repeatCount="indefinite"
|
||||
></animate>
|
||||
</rect>
|
||||
</g><g transform="rotate(30 50 50)">
|
||||
<rect x="49" y="24" rx="0" ry="0" width="2" height="12" fill="#20a5d6">
|
||||
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.8333333333333334s" repeatCount="indefinite"></animate>
|
||||
</g>
|
||||
<g transform="rotate(30 50 50)">
|
||||
<rect
|
||||
x="49"
|
||||
y="24"
|
||||
rx="0"
|
||||
ry="0"
|
||||
width="2"
|
||||
height="12"
|
||||
fill="#20a5d6"
|
||||
>
|
||||
<animate
|
||||
attributeName="opacity"
|
||||
values="1;0"
|
||||
keyTimes="0;1"
|
||||
dur="1s"
|
||||
begin="-0.8333333333333334s"
|
||||
repeatCount="indefinite"
|
||||
></animate>
|
||||
</rect>
|
||||
</g><g transform="rotate(60 50 50)">
|
||||
<rect x="49" y="24" rx="0" ry="0" width="2" height="12" fill="#20a5d6">
|
||||
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.75s" repeatCount="indefinite"></animate>
|
||||
</g>
|
||||
<g transform="rotate(60 50 50)">
|
||||
<rect
|
||||
x="49"
|
||||
y="24"
|
||||
rx="0"
|
||||
ry="0"
|
||||
width="2"
|
||||
height="12"
|
||||
fill="#20a5d6"
|
||||
>
|
||||
<animate
|
||||
attributeName="opacity"
|
||||
values="1;0"
|
||||
keyTimes="0;1"
|
||||
dur="1s"
|
||||
begin="-0.75s"
|
||||
repeatCount="indefinite"
|
||||
></animate>
|
||||
</rect>
|
||||
</g><g transform="rotate(90 50 50)">
|
||||
<rect x="49" y="24" rx="0" ry="0" width="2" height="12" fill="#20a5d6">
|
||||
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.6666666666666666s" repeatCount="indefinite"></animate>
|
||||
</g>
|
||||
<g transform="rotate(90 50 50)">
|
||||
<rect
|
||||
x="49"
|
||||
y="24"
|
||||
rx="0"
|
||||
ry="0"
|
||||
width="2"
|
||||
height="12"
|
||||
fill="#20a5d6"
|
||||
>
|
||||
<animate
|
||||
attributeName="opacity"
|
||||
values="1;0"
|
||||
keyTimes="0;1"
|
||||
dur="1s"
|
||||
begin="-0.6666666666666666s"
|
||||
repeatCount="indefinite"
|
||||
></animate>
|
||||
</rect>
|
||||
</g><g transform="rotate(120 50 50)">
|
||||
<rect x="49" y="24" rx="0" ry="0" width="2" height="12" fill="#20a5d6">
|
||||
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.5833333333333334s" repeatCount="indefinite"></animate>
|
||||
</g>
|
||||
<g transform="rotate(120 50 50)">
|
||||
<rect
|
||||
x="49"
|
||||
y="24"
|
||||
rx="0"
|
||||
ry="0"
|
||||
width="2"
|
||||
height="12"
|
||||
fill="#20a5d6"
|
||||
>
|
||||
<animate
|
||||
attributeName="opacity"
|
||||
values="1;0"
|
||||
keyTimes="0;1"
|
||||
dur="1s"
|
||||
begin="-0.5833333333333334s"
|
||||
repeatCount="indefinite"
|
||||
></animate>
|
||||
</rect>
|
||||
</g><g transform="rotate(150 50 50)">
|
||||
<rect x="49" y="24" rx="0" ry="0" width="2" height="12" fill="#20a5d6">
|
||||
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.5s" repeatCount="indefinite"></animate>
|
||||
</g>
|
||||
<g transform="rotate(150 50 50)">
|
||||
<rect
|
||||
x="49"
|
||||
y="24"
|
||||
rx="0"
|
||||
ry="0"
|
||||
width="2"
|
||||
height="12"
|
||||
fill="#20a5d6"
|
||||
>
|
||||
<animate
|
||||
attributeName="opacity"
|
||||
values="1;0"
|
||||
keyTimes="0;1"
|
||||
dur="1s"
|
||||
begin="-0.5s"
|
||||
repeatCount="indefinite"
|
||||
></animate>
|
||||
</rect>
|
||||
</g><g transform="rotate(180 50 50)">
|
||||
<rect x="49" y="24" rx="0" ry="0" width="2" height="12" fill="#20a5d6">
|
||||
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.4166666666666667s" repeatCount="indefinite"></animate>
|
||||
</g>
|
||||
<g transform="rotate(180 50 50)">
|
||||
<rect
|
||||
x="49"
|
||||
y="24"
|
||||
rx="0"
|
||||
ry="0"
|
||||
width="2"
|
||||
height="12"
|
||||
fill="#20a5d6"
|
||||
>
|
||||
<animate
|
||||
attributeName="opacity"
|
||||
values="1;0"
|
||||
keyTimes="0;1"
|
||||
dur="1s"
|
||||
begin="-0.4166666666666667s"
|
||||
repeatCount="indefinite"
|
||||
></animate>
|
||||
</rect>
|
||||
</g><g transform="rotate(210 50 50)">
|
||||
<rect x="49" y="24" rx="0" ry="0" width="2" height="12" fill="#20a5d6">
|
||||
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.3333333333333333s" repeatCount="indefinite"></animate>
|
||||
</g>
|
||||
<g transform="rotate(210 50 50)">
|
||||
<rect
|
||||
x="49"
|
||||
y="24"
|
||||
rx="0"
|
||||
ry="0"
|
||||
width="2"
|
||||
height="12"
|
||||
fill="#20a5d6"
|
||||
>
|
||||
<animate
|
||||
attributeName="opacity"
|
||||
values="1;0"
|
||||
keyTimes="0;1"
|
||||
dur="1s"
|
||||
begin="-0.3333333333333333s"
|
||||
repeatCount="indefinite"
|
||||
></animate>
|
||||
</rect>
|
||||
</g><g transform="rotate(240 50 50)">
|
||||
<rect x="49" y="24" rx="0" ry="0" width="2" height="12" fill="#20a5d6">
|
||||
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.25s" repeatCount="indefinite"></animate>
|
||||
</g>
|
||||
<g transform="rotate(240 50 50)">
|
||||
<rect
|
||||
x="49"
|
||||
y="24"
|
||||
rx="0"
|
||||
ry="0"
|
||||
width="2"
|
||||
height="12"
|
||||
fill="#20a5d6"
|
||||
>
|
||||
<animate
|
||||
attributeName="opacity"
|
||||
values="1;0"
|
||||
keyTimes="0;1"
|
||||
dur="1s"
|
||||
begin="-0.25s"
|
||||
repeatCount="indefinite"
|
||||
></animate>
|
||||
</rect>
|
||||
</g><g transform="rotate(270 50 50)">
|
||||
<rect x="49" y="24" rx="0" ry="0" width="2" height="12" fill="#20a5d6">
|
||||
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.16666666666666666s" repeatCount="indefinite"></animate>
|
||||
</g>
|
||||
<g transform="rotate(270 50 50)">
|
||||
<rect
|
||||
x="49"
|
||||
y="24"
|
||||
rx="0"
|
||||
ry="0"
|
||||
width="2"
|
||||
height="12"
|
||||
fill="#20a5d6"
|
||||
>
|
||||
<animate
|
||||
attributeName="opacity"
|
||||
values="1;0"
|
||||
keyTimes="0;1"
|
||||
dur="1s"
|
||||
begin="-0.16666666666666666s"
|
||||
repeatCount="indefinite"
|
||||
></animate>
|
||||
</rect>
|
||||
</g><g transform="rotate(300 50 50)">
|
||||
<rect x="49" y="24" rx="0" ry="0" width="2" height="12" fill="#20a5d6">
|
||||
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.08333333333333333s" repeatCount="indefinite"></animate>
|
||||
</g>
|
||||
<g transform="rotate(300 50 50)">
|
||||
<rect
|
||||
x="49"
|
||||
y="24"
|
||||
rx="0"
|
||||
ry="0"
|
||||
width="2"
|
||||
height="12"
|
||||
fill="#20a5d6"
|
||||
>
|
||||
<animate
|
||||
attributeName="opacity"
|
||||
values="1;0"
|
||||
keyTimes="0;1"
|
||||
dur="1s"
|
||||
begin="-0.08333333333333333s"
|
||||
repeatCount="indefinite"
|
||||
></animate>
|
||||
</rect>
|
||||
</g><g transform="rotate(330 50 50)">
|
||||
<rect x="49" y="24" rx="0" ry="0" width="2" height="12" fill="#20a5d6">
|
||||
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="0s" repeatCount="indefinite"></animate>
|
||||
</g>
|
||||
<g transform="rotate(330 50 50)">
|
||||
<rect
|
||||
x="49"
|
||||
y="24"
|
||||
rx="0"
|
||||
ry="0"
|
||||
width="2"
|
||||
height="12"
|
||||
fill="#20a5d6"
|
||||
>
|
||||
<animate
|
||||
attributeName="opacity"
|
||||
values="1;0"
|
||||
keyTimes="0;1"
|
||||
dur="1s"
|
||||
begin="0s"
|
||||
repeatCount="indefinite"
|
||||
></animate>
|
||||
</rect>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
@ -12,8 +12,8 @@ const PanelSection = ({
|
||||
expanded = false,
|
||||
loading = false,
|
||||
hideVisibleButton = false,
|
||||
onVisibilityChange = () => { },
|
||||
onExpandChange = () => { }
|
||||
onVisibilityChange = () => {},
|
||||
onExpandChange = () => {},
|
||||
}) => {
|
||||
const [isExpanded, setIsExpanded] = useState(expanded);
|
||||
const [isVisible, setIsVisible] = useState(visible);
|
||||
@ -27,7 +27,7 @@ const PanelSection = ({
|
||||
className="dcmrt-panel-section"
|
||||
style={{
|
||||
marginBottom: isExpanded ? 0 : 2,
|
||||
height: isExpanded ? '100%' : 'unset'
|
||||
height: isExpanded ? '100%' : 'unset',
|
||||
}}
|
||||
>
|
||||
<div className="header">
|
||||
@ -36,7 +36,7 @@ const PanelSection = ({
|
||||
{!hideVisibleButton && (
|
||||
<Icon
|
||||
className={`eye-icon ${isVisible && 'expanded'}`}
|
||||
name={isVisible ? "eye" : "eye-closed"}
|
||||
name={isVisible ? 'eye' : 'eye-closed'}
|
||||
width="20px"
|
||||
height="20px"
|
||||
onClick={() => {
|
||||
@ -47,7 +47,9 @@ const PanelSection = ({
|
||||
/>
|
||||
)}
|
||||
<Icon
|
||||
className={`angle-double-${isExpanded ? 'down' : 'up'} ${isExpanded && 'expanded'}`}
|
||||
className={`angle-double-${
|
||||
isExpanded ? 'down' : 'up'
|
||||
} ${isExpanded && 'expanded'}`}
|
||||
name={`angle-double-${isExpanded ? 'down' : 'up'}`}
|
||||
width="20px"
|
||||
height="20px"
|
||||
@ -59,7 +61,9 @@ const PanelSection = ({
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{loading && isExpanded && <LoadingIndicator expand height="70px" width="70px" />}
|
||||
{loading && isExpanded && (
|
||||
<LoadingIndicator expand height="70px" width="70px" />
|
||||
)}
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
@ -70,13 +74,13 @@ PanelSection.propTypes = {
|
||||
children: PropTypes.oneOfType([PropTypes.node, PropTypes.func]).isRequired,
|
||||
visible: PropTypes.bool,
|
||||
expanded: PropTypes.bool,
|
||||
onVisibilityChange: PropTypes.func
|
||||
onVisibilityChange: PropTypes.func,
|
||||
};
|
||||
|
||||
PanelSection.defaultProps = {
|
||||
visible: false,
|
||||
expanded: false,
|
||||
onVisibilityChange: () => { }
|
||||
onVisibilityChange: () => {},
|
||||
};
|
||||
|
||||
export default PanelSection;
|
||||
|
||||
@ -31,7 +31,13 @@ const refreshViewport = () => {
|
||||
* @param {number} props.isOpen - isOpen
|
||||
* @returns component
|
||||
*/
|
||||
const RTPanel = ({ studies, viewports, activeIndex, isOpen, onContourItemClick }) => {
|
||||
const RTPanel = ({
|
||||
studies,
|
||||
viewports,
|
||||
activeIndex,
|
||||
isOpen,
|
||||
onContourItemClick,
|
||||
}) => {
|
||||
const [selectedContour, setSelectedContour] = useState();
|
||||
const DEFAULT_SET_INDEX = 0;
|
||||
const DEFAULT_STATE = {
|
||||
@ -65,7 +71,10 @@ const RTPanel = ({ studies, viewports, activeIndex, isOpen, onContourItemClick }
|
||||
document.addEventListener('extensiondicomrtrtloaded', updateStructureSets);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('extensiondicomrtrtloaded', updateStructureSets);
|
||||
document.removeEventListener(
|
||||
'extensiondicomrtrtloaded',
|
||||
updateStructureSets
|
||||
);
|
||||
};
|
||||
}, []);
|
||||
|
||||
@ -78,7 +87,9 @@ const RTPanel = ({ studies, viewports, activeIndex, isOpen, onContourItemClick }
|
||||
activeViewport.SeriesInstanceUID
|
||||
);
|
||||
|
||||
const studyMetadata = studyMetadataManager.get(activeViewport.StudyInstanceUID);
|
||||
const studyMetadata = studyMetadataManager.get(
|
||||
activeViewport.StudyInstanceUID
|
||||
);
|
||||
const referencedDisplaysets = studyMetadata.getDerivedDatasets({
|
||||
referencedSeriesInstanceUID: activeViewport.SeriesInstanceUID,
|
||||
Modality: 'RTSTRUCT',
|
||||
@ -89,7 +100,7 @@ const RTPanel = ({ studies, viewports, activeIndex, isOpen, onContourItemClick }
|
||||
setState({
|
||||
referencedDisplaysets,
|
||||
selectedSet: defaultSet,
|
||||
sets: viewportSets
|
||||
sets: viewportSets,
|
||||
});
|
||||
} else {
|
||||
setState(DEFAULT_STATE);
|
||||
@ -105,7 +116,10 @@ const RTPanel = ({ studies, viewports, activeIndex, isOpen, onContourItemClick }
|
||||
setShowSettings(showSettings && !isOpen);
|
||||
}, [isOpen]);
|
||||
|
||||
const toContourItem = ({ ROINumber, ROIName, RTROIObservations, colorArray, visible }, loadedSet) => {
|
||||
const toContourItem = (
|
||||
{ ROINumber, ROIName, RTROIObservations, colorArray, visible },
|
||||
loadedSet
|
||||
) => {
|
||||
let interpretedType = '';
|
||||
if (RTROIObservations && RTROIObservations.RTROIInterpretedType) {
|
||||
interpretedType = `(${RTROIObservations.RTROIInterpretedType})`;
|
||||
@ -137,14 +151,20 @@ const RTPanel = ({ studies, viewports, activeIndex, isOpen, onContourItemClick }
|
||||
);
|
||||
|
||||
const frameIndex = imageIds.indexOf(imageId);
|
||||
const SOPInstanceUID = cornerstone.metaData.get('SOPInstanceUID', imageId);
|
||||
const StudyInstanceUID = cornerstone.metaData.get('StudyInstanceUID', imageId);
|
||||
const SOPInstanceUID = cornerstone.metaData.get(
|
||||
'SOPInstanceUID',
|
||||
imageId
|
||||
);
|
||||
const StudyInstanceUID = cornerstone.metaData.get(
|
||||
'StudyInstanceUID',
|
||||
imageId
|
||||
);
|
||||
|
||||
onContourItemClick({
|
||||
StudyInstanceUID,
|
||||
SOPInstanceUID,
|
||||
frameIndex,
|
||||
activeViewportIndex: activeIndex
|
||||
activeViewportIndex: activeIndex,
|
||||
});
|
||||
}}
|
||||
label={`${ROIName} ${interpretedType}`}
|
||||
@ -153,7 +173,10 @@ const RTPanel = ({ studies, viewports, activeIndex, isOpen, onContourItemClick }
|
||||
visible={visible}
|
||||
onVisibilityChange={() => {
|
||||
const module = cornerstoneTools.getModule('rtstruct');
|
||||
module.setters.toggleROIContour(state.selectedSet.SeriesInstanceUID, ROINumber);
|
||||
module.setters.toggleROIContour(
|
||||
state.selectedSet.SeriesInstanceUID,
|
||||
ROINumber
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
@ -189,49 +212,70 @@ const RTPanel = ({ studies, viewports, activeIndex, isOpen, onContourItemClick }
|
||||
onClick={() => setShowSettings(true)}
|
||||
/>
|
||||
</div>
|
||||
{!state.referencedDisplaysets.length && <LoadingIndicator expand height="70px" width="70px" />}
|
||||
{state.sets && state.referencedDisplaysets.map(displaySet => {
|
||||
const { SeriesInstanceUID, metadata, isLoaded } = displaySet;
|
||||
{!state.referencedDisplaysets.length && (
|
||||
<LoadingIndicator expand height="70px" width="70px" />
|
||||
)}
|
||||
{state.sets &&
|
||||
state.referencedDisplaysets.map(displaySet => {
|
||||
const { SeriesInstanceUID, metadata, isLoaded } = displaySet;
|
||||
|
||||
const module = cornerstoneTools.getModule('rtstruct');
|
||||
const sets = module.getters.structuresSetsWhichReferenceSeriesInstanceUid(viewports[activeIndex].SeriesInstanceUID);
|
||||
const module = cornerstoneTools.getModule('rtstruct');
|
||||
const sets = module.getters.structuresSetsWhichReferenceSeriesInstanceUid(
|
||||
viewports[activeIndex].SeriesInstanceUID
|
||||
);
|
||||
|
||||
const loadedSet = sets.find(set => set.SeriesInstanceUID === SeriesInstanceUID);
|
||||
return (
|
||||
<PanelSection
|
||||
key={SeriesInstanceUID}
|
||||
title={metadata.StructureSetLabel}
|
||||
loading={!isLoaded || !loadedSet}
|
||||
visible={isLoaded && loadedSet.visible}
|
||||
hideVisibleButton={!isLoaded}
|
||||
expanded={isLoaded && loadedSet.SeriesInstanceUID === state.selectedSet.SeriesInstanceUID}
|
||||
onVisibilityChange={newVisibility => {
|
||||
const module = cornerstoneTools.getModule('rtstruct');
|
||||
loadedSet.ROIContours.forEach(({ ROINumber }) => {
|
||||
module.setters.toggleROIContour(loadedSet.SeriesInstanceUID, ROINumber);
|
||||
});
|
||||
const sets = module.getters.structuresSetsWhichReferenceSeriesInstanceUid(viewports[activeIndex].SeriesInstanceUID);
|
||||
setState(state => ({ ...state, sets }));
|
||||
refreshViewport();
|
||||
}}
|
||||
onExpandChange={async () => {
|
||||
if (!isLoaded) {
|
||||
await displaySet.load(viewports[activeIndex], studies);
|
||||
const module = cornerstoneTools.getModule('rtstruct');
|
||||
const sets = module.getters.structuresSetsWhichReferenceSeriesInstanceUid(viewports[activeIndex].SeriesInstanceUID);
|
||||
const selectedSet = sets.find(set => set.SeriesInstanceUID === SeriesInstanceUID);
|
||||
setState(state => ({ ...state, selectedSet, sets }));
|
||||
const loadedSet = sets.find(
|
||||
set => set.SeriesInstanceUID === SeriesInstanceUID
|
||||
);
|
||||
return (
|
||||
<PanelSection
|
||||
key={SeriesInstanceUID}
|
||||
title={metadata.StructureSetLabel}
|
||||
loading={!isLoaded || !loadedSet}
|
||||
visible={isLoaded && loadedSet.visible}
|
||||
hideVisibleButton={!isLoaded}
|
||||
expanded={
|
||||
isLoaded &&
|
||||
loadedSet.SeriesInstanceUID ===
|
||||
state.selectedSet.SeriesInstanceUID
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ScrollableArea>
|
||||
<TableList headless>
|
||||
{isLoaded && loadedSet.ROIContours.map(c => toContourItem(c, loadedSet))}
|
||||
</TableList>
|
||||
</ScrollableArea>
|
||||
</PanelSection>
|
||||
);
|
||||
})}
|
||||
onVisibilityChange={newVisibility => {
|
||||
const module = cornerstoneTools.getModule('rtstruct');
|
||||
loadedSet.ROIContours.forEach(({ ROINumber }) => {
|
||||
module.setters.toggleROIContour(
|
||||
loadedSet.SeriesInstanceUID,
|
||||
ROINumber
|
||||
);
|
||||
});
|
||||
const sets = module.getters.structuresSetsWhichReferenceSeriesInstanceUid(
|
||||
viewports[activeIndex].SeriesInstanceUID
|
||||
);
|
||||
setState(state => ({ ...state, sets }));
|
||||
refreshViewport();
|
||||
}}
|
||||
onExpandChange={async () => {
|
||||
if (!isLoaded) {
|
||||
await displaySet.load(viewports[activeIndex], studies);
|
||||
const module = cornerstoneTools.getModule('rtstruct');
|
||||
const sets = module.getters.structuresSetsWhichReferenceSeriesInstanceUid(
|
||||
viewports[activeIndex].SeriesInstanceUID
|
||||
);
|
||||
const selectedSet = sets.find(
|
||||
set => set.SeriesInstanceUID === SeriesInstanceUID
|
||||
);
|
||||
setState(state => ({ ...state, selectedSet, sets }));
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ScrollableArea>
|
||||
<TableList headless>
|
||||
{isLoaded &&
|
||||
loadedSet.ROIContours.map(c => toContourItem(c, loadedSet))}
|
||||
</TableList>
|
||||
</ScrollableArea>
|
||||
</PanelSection>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -15,9 +15,7 @@ const RTSettings = ({ configuration, onBack, onChange }) => {
|
||||
return (
|
||||
<div className="settings-section">
|
||||
<div className="header">{title}</div>
|
||||
<div className="content">
|
||||
{children}
|
||||
</div>
|
||||
<div className="content">{children}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -61,8 +59,10 @@ const RTSettings = ({ configuration, onBack, onChange }) => {
|
||||
|
||||
RTSettings.propTypes = {
|
||||
configuration: PropTypes.shape({
|
||||
lineWidth: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired,
|
||||
opacity: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired,
|
||||
lineWidth: PropTypes.oneOfType([PropTypes.number, PropTypes.string])
|
||||
.isRequired,
|
||||
opacity: PropTypes.oneOfType([PropTypes.number, PropTypes.string])
|
||||
.isRequired,
|
||||
}).isRequired,
|
||||
onBack: PropTypes.func.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
|
||||
@ -31,7 +31,7 @@ const StructureSetItem = ({
|
||||
|
||||
useEffect(() => {
|
||||
setIsVisible(visible);
|
||||
}, [visible])
|
||||
}, [visible]);
|
||||
|
||||
return (
|
||||
<div className={`dcmrt-structure-set-item ${selected && 'selected'}`}>
|
||||
@ -99,7 +99,7 @@ StructureSetItem.propTypes = {
|
||||
|
||||
StructureSetItem.defaultProps = {
|
||||
itemClass: '',
|
||||
onClick: () => { },
|
||||
onClick: () => {},
|
||||
};
|
||||
|
||||
export default StructureSetItem;
|
||||
|
||||
@ -68,7 +68,6 @@ export default async function loadRTStruct(
|
||||
const rtStructDisplayToolName = TOOL_NAMES.RTSTRUCT_DISPLAY_TOOL;
|
||||
|
||||
for (let i = 0; i < ROIContourSequence.length; i++) {
|
||||
|
||||
const ROIContour = ROIContourSequence[i];
|
||||
const { ReferencedROINumber, ContourSequence } = ROIContour;
|
||||
|
||||
|
||||
@ -70,9 +70,7 @@ export default class RTStructDisplayTool extends BaseTool {
|
||||
}
|
||||
|
||||
const colorArray = ROIContourData.colorArray;
|
||||
const color = `rgba(${colorArray[0]},${colorArray[1]},${
|
||||
colorArray[2]
|
||||
},${opacity})`;
|
||||
const color = `rgba(${colorArray[0]},${colorArray[1]},${colorArray[2]},${opacity})`;
|
||||
|
||||
lineWidth;
|
||||
|
||||
|
||||
@ -10,8 +10,6 @@ export default function structureSetReferencesSeriesInstanceUid(
|
||||
) {
|
||||
const { referencedSeriesSequence } = StructureSet;
|
||||
return referencedSeriesSequence.some(
|
||||
referencedSeries =>
|
||||
referencedSeries.SeriesInstanceUID ===
|
||||
SeriesInstanceUID
|
||||
referencedSeries => referencedSeries.SeriesInstanceUID === SeriesInstanceUID
|
||||
);
|
||||
}
|
||||
|
||||
@ -154,15 +154,10 @@ function setToggleROIContour(SeriesInstanceUID, ROINumber) {
|
||||
* Returns an array of StructureSets which reference the given SeriesInstanceUID.
|
||||
* @param {string} SeriesInstanceUID The SeriesInstanceUID to check.
|
||||
*/
|
||||
function getStructuresSetsWhichReferenceSeriesInstanceUid(
|
||||
SeriesInstanceUID
|
||||
) {
|
||||
function getStructuresSetsWhichReferenceSeriesInstanceUid(SeriesInstanceUID) {
|
||||
const { StructureSets } = state;
|
||||
return StructureSets.filter(StructureSet =>
|
||||
structureSetReferencesSeriesInstanceUid(
|
||||
StructureSet,
|
||||
SeriesInstanceUID
|
||||
)
|
||||
structureSetReferencesSeriesInstanceUid(StructureSet, SeriesInstanceUID)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -12,12 +12,8 @@ const BrushColorSelector = ({ defaultColor, index, onNext, onPrev }) => (
|
||||
{index}
|
||||
</div>
|
||||
<div className="selector-buttons">
|
||||
<button onClick={onPrev}>
|
||||
Previous
|
||||
</button>
|
||||
<button onClick={onNext}>
|
||||
Next
|
||||
</button>
|
||||
<button onClick={onPrev}>Previous</button>
|
||||
<button onClick={onNext}>Next</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -27,7 +27,7 @@ BrushRadius.propTypes = {
|
||||
};
|
||||
|
||||
BrushRadius.defaultProps = {
|
||||
onChange: () => { },
|
||||
onChange: () => {},
|
||||
min: 1,
|
||||
max: 50,
|
||||
step: 1,
|
||||
|
||||
@ -17,7 +17,15 @@ ColoredCircle.propTypes = {
|
||||
color: PropTypes.array.isRequired,
|
||||
};
|
||||
|
||||
const SegmentItem = ({ index, label, onClick, itemClass, color, visible = true, onVisibilityChange }) => {
|
||||
const SegmentItem = ({
|
||||
index,
|
||||
label,
|
||||
onClick,
|
||||
itemClass,
|
||||
color,
|
||||
visible = true,
|
||||
onVisibilityChange,
|
||||
}) => {
|
||||
const [isVisible, setIsVisible] = useState(visible);
|
||||
|
||||
useEffect(() => {
|
||||
@ -61,7 +69,7 @@ const SegmentItem = ({ index, label, onClick, itemClass, color, visible = true,
|
||||
<span style={{ marginRight: '4px' }}>
|
||||
<Icon name="edit" width="14px" height="14px" />
|
||||
</span>
|
||||
Relabel
|
||||
Relabel
|
||||
</button>
|
||||
<button
|
||||
className="btnAction"
|
||||
@ -70,7 +78,7 @@ const SegmentItem = ({ index, label, onClick, itemClass, color, visible = true,
|
||||
<span style={{ marginRight: '4px' }}>
|
||||
<Icon name="edit" width="14px" height="14px" />
|
||||
</span>
|
||||
Description
|
||||
Description
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
@ -90,7 +98,7 @@ SegmentItem.propTypes = {
|
||||
|
||||
SegmentItem.defaultProps = {
|
||||
itemClass: '',
|
||||
onClick: () => { }
|
||||
onClick: () => {},
|
||||
};
|
||||
|
||||
export default SegmentItem;
|
||||
|
||||
@ -21,7 +21,7 @@ SegmentationItem.propTypes = {
|
||||
};
|
||||
|
||||
SegmentationItem.defaultProps = {
|
||||
description: ''
|
||||
description: '',
|
||||
};
|
||||
|
||||
export default SegmentationItem;
|
||||
|
||||
@ -68,8 +68,7 @@ const SegmentationPanel = ({
|
||||
*/
|
||||
const [state, setState] = useState({
|
||||
brushRadius: DEFAULT_BRUSH_RADIUS,
|
||||
brushColor:
|
||||
'rgba(221, 85, 85, 1)',
|
||||
brushColor: 'rgba(221, 85, 85, 1)',
|
||||
selectedSegment: null,
|
||||
selectedSegmentation: null,
|
||||
showSegmentationSettings: false,
|
||||
@ -77,7 +76,7 @@ const SegmentationPanel = ({
|
||||
labelmapList: [],
|
||||
segmentList: [],
|
||||
cachedSegmentsProperties: [],
|
||||
isLoading: false
|
||||
isLoading: false,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
@ -111,7 +110,10 @@ const SegmentationPanel = ({
|
||||
* allows us to easily watch the module or the segmentations loading process in any other component
|
||||
* without subscribing to external events.
|
||||
*/
|
||||
document.addEventListener('extensiondicomsegmentationsegloaded', refreshSegmentations);
|
||||
document.addEventListener(
|
||||
'extensiondicomsegmentationsegloaded',
|
||||
refreshSegmentations
|
||||
);
|
||||
|
||||
/*
|
||||
* These are specific to each element;
|
||||
@ -126,7 +128,10 @@ const SegmentationPanel = ({
|
||||
);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('extensiondicomsegmentationsegloaded', refreshSegmentations);
|
||||
document.removeEventListener(
|
||||
'extensiondicomsegmentationsegloaded',
|
||||
refreshSegmentations
|
||||
);
|
||||
cornerstoneTools.store.state.enabledElements.forEach(enabledElement =>
|
||||
enabledElement.removeEventListener(
|
||||
'cornerstonetoolslabelmapmodified',
|
||||
@ -173,15 +178,17 @@ const SegmentationPanel = ({
|
||||
segmentList: [],
|
||||
}));
|
||||
}
|
||||
}, [
|
||||
viewports,
|
||||
activeIndex,
|
||||
state.isLoading
|
||||
]);
|
||||
}, [viewports, activeIndex, state.isLoading]);
|
||||
|
||||
useEffect(() => {
|
||||
refreshSegmentations();
|
||||
}, [viewports, activeIndex, state.selectedSegmentation, activeContexts, state.isLoading]);
|
||||
}, [
|
||||
viewports,
|
||||
activeIndex,
|
||||
state.selectedSegmentation,
|
||||
activeContexts,
|
||||
state.isLoading,
|
||||
]);
|
||||
|
||||
/* Handle open/closed panel behaviour */
|
||||
useEffect(() => {
|
||||
@ -369,9 +376,13 @@ const SegmentationPanel = ({
|
||||
return !segmentsHidden[segmentIndex];
|
||||
};
|
||||
|
||||
const cachedSegmentProperties = state.cachedSegmentsProperties[segmentNumber];
|
||||
const cachedSegmentProperties =
|
||||
state.cachedSegmentsProperties[segmentNumber];
|
||||
let visible = isSegmentVisible();
|
||||
if (cachedSegmentProperties && cachedSegmentProperties.visible !== visible) {
|
||||
if (
|
||||
cachedSegmentProperties &&
|
||||
cachedSegmentProperties.visible !== visible
|
||||
) {
|
||||
toggleSegmentVisibility();
|
||||
}
|
||||
|
||||
@ -399,7 +410,9 @@ const SegmentationPanel = ({
|
||||
onSegmentVisibilityChange(segmentNumber, newVisibility);
|
||||
}
|
||||
|
||||
updateCachedSegmentsProperties(segmentNumber, { visible: newVisibility });
|
||||
updateCachedSegmentsProperties(segmentNumber, {
|
||||
visible: newVisibility,
|
||||
});
|
||||
refreshViewport();
|
||||
}}
|
||||
/>
|
||||
@ -422,10 +435,9 @@ const SegmentationPanel = ({
|
||||
const segmentsProperties = state.cachedSegmentsProperties;
|
||||
const segmentProperties = state.cachedSegmentsProperties[segmentNumber];
|
||||
|
||||
segmentsProperties[segmentNumber] =
|
||||
segmentProperties ?
|
||||
{ ...segmentProperties, ...properties } :
|
||||
properties;
|
||||
segmentsProperties[segmentNumber] = segmentProperties
|
||||
? { ...segmentProperties, ...properties }
|
||||
: properties;
|
||||
|
||||
updateState('cachedSegmentsProperties', segmentsProperties);
|
||||
};
|
||||
@ -495,7 +507,10 @@ const SegmentationPanel = ({
|
||||
refreshViewport();
|
||||
};
|
||||
|
||||
const disabledConfigurationFields = ['outlineAlpha', 'shouldRenderInactiveLabelmaps'];
|
||||
const disabledConfigurationFields = [
|
||||
'outlineAlpha',
|
||||
'shouldRenderInactiveLabelmaps',
|
||||
];
|
||||
if (state.showSegmentationSettings) {
|
||||
return (
|
||||
<SegmentationSettings
|
||||
@ -622,7 +637,7 @@ const _setActiveLabelmap = async (
|
||||
displaySet,
|
||||
firstImageId,
|
||||
activeLabelmapIndex,
|
||||
callback = () => { },
|
||||
callback = () => {},
|
||||
onDisplaySetLoadFailure
|
||||
) => {
|
||||
if (displaySet.labelmapIndex === activeLabelmapIndex) {
|
||||
|
||||
@ -19,7 +19,7 @@ const uiGrayDark = computedstyle.getPropertyValue('--ui-gray-dark');
|
||||
const segmentationSelectStyles = {
|
||||
singleValue: (base, state) => ({
|
||||
...base,
|
||||
width: '100%'
|
||||
width: '100%',
|
||||
}),
|
||||
control: (base, state) => ({
|
||||
...base,
|
||||
|
||||
@ -4,7 +4,12 @@ import { Range } from '@ohif/ui';
|
||||
|
||||
import './SegmentationSettings.css';
|
||||
|
||||
const SegmentationSettings = ({ configuration, onBack, onChange, disabledFields = [] }) => {
|
||||
const SegmentationSettings = ({
|
||||
configuration,
|
||||
onBack,
|
||||
onChange,
|
||||
disabledFields = [],
|
||||
}) => {
|
||||
const [state, setState] = useState({
|
||||
renderFill: configuration.renderFill,
|
||||
renderOutline: configuration.renderOutline,
|
||||
@ -13,7 +18,7 @@ const SegmentationSettings = ({ configuration, onBack, onChange, disabledFields
|
||||
outlineAlpha: configuration.outlineAlpha,
|
||||
outlineWidth: configuration.outlineWidth,
|
||||
fillAlphaInactive: configuration.fillAlphaInactive,
|
||||
outlineAlphaInactive: configuration.outlineAlphaInactive
|
||||
outlineAlphaInactive: configuration.outlineAlphaInactive,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
@ -78,7 +83,9 @@ const SegmentationSettings = ({ configuration, onBack, onChange, disabledFields
|
||||
step={1}
|
||||
min={0}
|
||||
max={100}
|
||||
onChange={event => save('outlineAlpha', toFloat(event.target.value))}
|
||||
onChange={event =>
|
||||
save('outlineAlpha', toFloat(event.target.value))
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{!disabledFields.includes('outlineWidth') && (
|
||||
@ -89,50 +96,64 @@ const SegmentationSettings = ({ configuration, onBack, onChange, disabledFields
|
||||
step={1}
|
||||
min={0}
|
||||
max={5}
|
||||
onChange={event => save('outlineWidth', parseInt(event.target.value))}
|
||||
onChange={event =>
|
||||
save('outlineWidth', parseInt(event.target.value))
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{(state.renderFill || state.renderOutline) && !disabledFields.includes('shouldRenderInactiveLabelmaps') && (
|
||||
<div
|
||||
className="settings-group"
|
||||
style={{ marginBottom: state.shouldRenderInactiveLabelmaps ? 15 : 0 }}
|
||||
>
|
||||
<CustomCheck
|
||||
label="Render inactive segmentations"
|
||||
checked={state.shouldRenderInactiveLabelmaps}
|
||||
onChange={() => check('shouldRenderInactiveLabelmaps')}
|
||||
/>
|
||||
{state.shouldRenderInactiveLabelmaps && (
|
||||
<>
|
||||
{state.renderFill && !disabledFields.includes('fillAlphaInactive') && (
|
||||
<CustomRange
|
||||
label="Fill Opacity"
|
||||
showPercentage
|
||||
step={1}
|
||||
min={0}
|
||||
max={100}
|
||||
value={state.fillAlphaInactive * 100}
|
||||
onChange={event => save('fillAlphaInactive', toFloat(event.target.value))}
|
||||
/>
|
||||
)}
|
||||
{state.renderOutline && !disabledFields.includes('outlineAlphaInactive') && (
|
||||
<CustomRange
|
||||
label="Outline Opacity"
|
||||
showPercentage
|
||||
step={1}
|
||||
min={0}
|
||||
max={100}
|
||||
value={state.outlineAlphaInactive * 100}
|
||||
onChange={event => save('outlineAlphaInactive', toFloat(event.target.value))}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{(state.renderFill || state.renderOutline) &&
|
||||
!disabledFields.includes('shouldRenderInactiveLabelmaps') && (
|
||||
<div
|
||||
className="settings-group"
|
||||
style={{
|
||||
marginBottom: state.shouldRenderInactiveLabelmaps ? 15 : 0,
|
||||
}}
|
||||
>
|
||||
<CustomCheck
|
||||
label="Render inactive segmentations"
|
||||
checked={state.shouldRenderInactiveLabelmaps}
|
||||
onChange={() => check('shouldRenderInactiveLabelmaps')}
|
||||
/>
|
||||
{state.shouldRenderInactiveLabelmaps && (
|
||||
<>
|
||||
{state.renderFill &&
|
||||
!disabledFields.includes('fillAlphaInactive') && (
|
||||
<CustomRange
|
||||
label="Fill Opacity"
|
||||
showPercentage
|
||||
step={1}
|
||||
min={0}
|
||||
max={100}
|
||||
value={state.fillAlphaInactive * 100}
|
||||
onChange={event =>
|
||||
save('fillAlphaInactive', toFloat(event.target.value))
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{state.renderOutline &&
|
||||
!disabledFields.includes('outlineAlphaInactive') && (
|
||||
<CustomRange
|
||||
label="Outline Opacity"
|
||||
showPercentage
|
||||
step={1}
|
||||
min={0}
|
||||
max={100}
|
||||
value={state.outlineAlphaInactive * 100}
|
||||
onChange={event =>
|
||||
save(
|
||||
'outlineAlphaInactive',
|
||||
toFloat(event.target.value)
|
||||
)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -169,8 +190,10 @@ SegmentationSettings.propTypes = {
|
||||
renderFill: PropTypes.bool.isRequired,
|
||||
renderOutline: PropTypes.bool.isRequired,
|
||||
shouldRenderInactiveLabelmaps: PropTypes.bool.isRequired,
|
||||
fillAlpha: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, /* TODO: why fillAlpha is string? */
|
||||
outlineAlpha: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, /* TODO: why fillAlpha is string? */
|
||||
fillAlpha: PropTypes.oneOfType([PropTypes.string, PropTypes.number])
|
||||
.isRequired /* TODO: why fillAlpha is string? */,
|
||||
outlineAlpha: PropTypes.oneOfType([PropTypes.string, PropTypes.number])
|
||||
.isRequired /* TODO: why fillAlpha is string? */,
|
||||
outlineWidth: PropTypes.number.isRequired,
|
||||
fillAlphaInactive: PropTypes.number.isRequired,
|
||||
outlineAlphaInactive: PropTypes.number.isRequired,
|
||||
|
||||
@ -46,7 +46,7 @@ export default {
|
||||
const onSegmentVisibilityChangeHandler = (segmentNumber, visible) => {
|
||||
commandsManager.runCommand('setSegmentConfiguration', {
|
||||
segmentNumber,
|
||||
visible
|
||||
visible,
|
||||
});
|
||||
};
|
||||
|
||||
@ -55,7 +55,7 @@ export default {
|
||||
globalOpacity: configuration.fillAlpha,
|
||||
outlineThickness: configuration.outlineWidth,
|
||||
renderOutline: configuration.renderOutline,
|
||||
visible: configuration.renderFill
|
||||
visible: configuration.renderFill,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@ -32,16 +32,16 @@ class LoadingIndicator extends PureComponent {
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="imageViewerLoadingIndicator loadingIndicator">
|
||||
<div className="indicatorContents">
|
||||
<p>
|
||||
{this.props.t('Loading...')}
|
||||
<i className="fa fa-spin fa-circle-o-notch fa-fw" />
|
||||
{percComplete}
|
||||
</p>
|
||||
</div>
|
||||
<div className="imageViewerLoadingIndicator loadingIndicator">
|
||||
<div className="indicatorContents">
|
||||
<p>
|
||||
{this.props.t('Loading...')}
|
||||
<i className="fa fa-spin fa-circle-o-notch fa-fw" />
|
||||
{percComplete}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
@ -49,7 +49,7 @@ class OHIFVTKViewport extends Component {
|
||||
state = {
|
||||
volumes: null,
|
||||
paintFilterLabelMapImageData: null,
|
||||
paintFilterBackgroundImageData: null
|
||||
paintFilterBackgroundImageData: null,
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
@ -69,7 +69,7 @@ class OHIFVTKViewport extends Component {
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
onScroll: () => { },
|
||||
onScroll: () => {},
|
||||
};
|
||||
|
||||
static id = 'OHIFVTKViewport';
|
||||
@ -156,9 +156,11 @@ class OHIFVTKViewport extends Component {
|
||||
const { activeLabelmapIndex } = brushStackState;
|
||||
const labelmap3D = brushStackState.labelmaps3D[activeLabelmapIndex];
|
||||
|
||||
this.segmentsDefaultProperties = labelmap3D.segmentsHidden.map(isHidden => {
|
||||
return { visible: !isHidden };
|
||||
});
|
||||
this.segmentsDefaultProperties = labelmap3D.segmentsHidden.map(
|
||||
isHidden => {
|
||||
return { visible: !isHidden };
|
||||
}
|
||||
);
|
||||
|
||||
const vtkLabelmapID = `${firstImageId}_${activeLabelmapIndex}`;
|
||||
|
||||
@ -349,7 +351,7 @@ class OHIFVTKViewport extends Component {
|
||||
|
||||
if (
|
||||
displaySet.displaySetInstanceUID !==
|
||||
prevDisplaySet.displaySetInstanceUID ||
|
||||
prevDisplaySet.displaySetInstanceUID ||
|
||||
displaySet.SOPInstanceUID !== prevDisplaySet.SOPInstanceUID ||
|
||||
displaySet.frameIndex !== prevDisplaySet.frameIndex
|
||||
) {
|
||||
@ -435,7 +437,7 @@ class OHIFVTKViewport extends Component {
|
||||
segmentsDefaultProperties: this.segmentsDefaultProperties,
|
||||
onNewSegmentationRequested: () => {
|
||||
this.setStateFromProps();
|
||||
}
|
||||
},
|
||||
}}
|
||||
onScroll={this.props.onScroll}
|
||||
/>
|
||||
|
||||
@ -165,7 +165,7 @@ const commandsModule = ({ commandsManager }) => {
|
||||
segmentNumber,
|
||||
frameIndex,
|
||||
frame,
|
||||
done = () => { }
|
||||
done = () => {},
|
||||
}) => {
|
||||
let api = apis[viewports.activeViewportIndex];
|
||||
|
||||
@ -180,10 +180,13 @@ const commandsModule = ({ commandsManager }) => {
|
||||
displaySetInstanceUID,
|
||||
SOPClassUID,
|
||||
SOPInstanceUID,
|
||||
frameIndex,
|
||||
frameIndex
|
||||
);
|
||||
|
||||
const imageDataObject = getImageData(stack.imageIds, displaySetInstanceUID);
|
||||
const imageDataObject = getImageData(
|
||||
stack.imageIds,
|
||||
displaySetInstanceUID
|
||||
);
|
||||
|
||||
let pixelIndex = 0;
|
||||
let x = 0;
|
||||
@ -209,7 +212,10 @@ const commandsModule = ({ commandsManager }) => {
|
||||
y /= count;
|
||||
|
||||
const position = [x, y, frameIndex];
|
||||
const worldPos = _convertModelToWorldSpace(position, imageDataObject.vtkImageData);
|
||||
const worldPos = _convertModelToWorldSpace(
|
||||
position,
|
||||
imageDataObject.vtkImageData
|
||||
);
|
||||
|
||||
api.svgWidgets.crosshairsWidget.moveCrosshairs(worldPos, apis);
|
||||
done();
|
||||
|
||||
@ -8,10 +8,13 @@ const { studyMetadataManager } = utils;
|
||||
|
||||
let isVisible = true;
|
||||
|
||||
const _isDisplaySetReconstructable = (viewportSpecificData = {}, activeViewportIndex) => {
|
||||
const _isDisplaySetReconstructable = (
|
||||
viewportSpecificData = {},
|
||||
activeViewportIndex
|
||||
) => {
|
||||
if (!viewportSpecificData[activeViewportIndex]) {
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
const { displaySetInstanceUID, StudyInstanceUID } = viewportSpecificData[
|
||||
activeViewportIndex
|
||||
@ -27,11 +30,13 @@ const _isDisplaySetReconstructable = (viewportSpecificData = {}, activeViewportI
|
||||
return false;
|
||||
}
|
||||
|
||||
const displaySet = study._displaySets.find(set => set.displaySetInstanceUID === displaySetInstanceUID);
|
||||
const displaySet = study._displaySets.find(
|
||||
set => set.displaySetInstanceUID === displaySetInstanceUID
|
||||
);
|
||||
|
||||
if (!displaySet) {
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
return displaySet.isReconstructable;
|
||||
};
|
||||
@ -52,12 +57,12 @@ function VTKMPRToolbarButton({
|
||||
return {
|
||||
viewportSpecificData,
|
||||
activeViewportIndex,
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
isVisible = _isDisplaySetReconstructable(
|
||||
viewportSpecificData,
|
||||
activeViewportIndex,
|
||||
activeViewportIndex
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@ -131,4 +131,4 @@ const definitions = [
|
||||
export default {
|
||||
definitions,
|
||||
defaultContext: 'ACTIVE_VIEWPORT::VTK',
|
||||
}
|
||||
};
|
||||
|
||||
@ -39,7 +39,8 @@
|
||||
"@babel/runtime": "7.7.6",
|
||||
"ajv": "^6.10.0",
|
||||
"dcmjs": "^0.12.4",
|
||||
"dicomweb-client": "^0.5.2",
|
||||
"dicomweb-client": "^0.6.0",
|
||||
"immer": "6.0.2",
|
||||
"isomorphic-base64": "^1.0.2",
|
||||
"lodash.clonedeep": "^4.5.0",
|
||||
"lodash.merge": "^4.6.1",
|
||||
|
||||
@ -79,7 +79,7 @@ export class StudyMetadata extends Metadata {
|
||||
Object.defineProperty(this, 'studyInstanceUID', {
|
||||
configurable: false,
|
||||
enumerable: false,
|
||||
get: function () {
|
||||
get: function() {
|
||||
return this.getStudyInstanceUID();
|
||||
},
|
||||
});
|
||||
|
||||
@ -182,7 +182,7 @@ export default class ExtensionManager {
|
||||
commandsManager: this._commandsManager,
|
||||
appConfig: this._appConfig,
|
||||
configuration,
|
||||
api: this._api
|
||||
api: this._api,
|
||||
});
|
||||
|
||||
if (!extensionModule) {
|
||||
|
||||
@ -7,6 +7,7 @@ import classes, { CommandsManager, HotkeysManager } from './classes/';
|
||||
import DICOMWeb from './DICOMWeb';
|
||||
import DICOMSR from './DICOMSR';
|
||||
import cornerstone from './cornerstone.js';
|
||||
import errorHandler from './errorHandler.js';
|
||||
import hangingProtocols from './hanging-protocols';
|
||||
import header from './header.js';
|
||||
import log from './log.js';
|
||||
|
||||
@ -2,7 +2,12 @@ import redux from './index.js';
|
||||
|
||||
describe('redux exports', () => {
|
||||
test('have not changed', () => {
|
||||
const expectedExports = ['actions', 'reducers', 'localStorage', 'sessionStorage'].sort();
|
||||
const expectedExports = [
|
||||
'actions',
|
||||
'reducers',
|
||||
'localStorage',
|
||||
'sessionStorage',
|
||||
].sort();
|
||||
|
||||
const exports = Object.keys(redux).sort();
|
||||
|
||||
|
||||
@ -34,7 +34,10 @@ describe('MeasurementService.js', () => {
|
||||
unit: 'mm',
|
||||
area: 123,
|
||||
type: measurementService.VALUE_TYPES.POLYLINE,
|
||||
points: [{ x: 1, y: 2 }, { x: 1, y: 2 }],
|
||||
points: [
|
||||
{ x: 1, y: 2 },
|
||||
{ x: 1, y: 2 },
|
||||
],
|
||||
source: source,
|
||||
};
|
||||
toAnnotation = () => annotation;
|
||||
|
||||
@ -1,6 +1,4 @@
|
||||
|
||||
export default class Queue {
|
||||
|
||||
constructor(limit) {
|
||||
this.limit = limit;
|
||||
this.size = 0;
|
||||
|
||||
@ -167,8 +167,8 @@ function _getSpacingIssue(spacing, averageSpacing) {
|
||||
function _getPerpendicularDistance(a, b) {
|
||||
return Math.sqrt(
|
||||
Math.pow(a[0] - b[0], 2) +
|
||||
Math.pow(a[1] - b[1], 2) +
|
||||
Math.pow(a[2] - b[2], 2)
|
||||
Math.pow(a[1] - b[1], 2) +
|
||||
Math.pow(a[2] - b[2], 2)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
export default function makeDeferred() {
|
||||
let reject, resolve, promise = new Promise(function (res, rej) {
|
||||
resolve = res;
|
||||
reject = rej;
|
||||
});
|
||||
let reject,
|
||||
resolve,
|
||||
promise = new Promise(function(res, rej) {
|
||||
resolve = res;
|
||||
reject = rej;
|
||||
});
|
||||
return Object.freeze({ promise, resolve, reject });
|
||||
}
|
||||
|
||||
@ -116,9 +116,8 @@ function _getPaletteColor(server, paletteColorLookupTableData, lutDescriptor) {
|
||||
const bits = lutDescriptor[2];
|
||||
|
||||
const arrayBufferToPaletteColorLUT = arraybuffer => {
|
||||
const byteArray = bits === 16 ?
|
||||
new Uint16Array(arraybuffer) :
|
||||
new Uint8Array(arraybuffer);
|
||||
const byteArray =
|
||||
bits === 16 ? new Uint16Array(arraybuffer) : new Uint8Array(arraybuffer);
|
||||
const lut = [];
|
||||
|
||||
for (let i = 0; i < numLutEntries; i++) {
|
||||
|
||||
@ -124,7 +124,7 @@ export default function getPixelSpacingInformation(instance) {
|
||||
};
|
||||
}
|
||||
|
||||
log.info(
|
||||
log.info(
|
||||
'Unknown combination of PixelSpacing and ImagerPixelSpacing identified. Unable to determine spacing.'
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import UserPreferencesModal from './UserPreferencesModal.json';
|
||||
|
||||
export default {
|
||||
'ar': {
|
||||
export default {
|
||||
ar: {
|
||||
UserPreferencesModal,
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@ -9,7 +9,7 @@ import StudyList from './StudyList.json';
|
||||
import UserPreferencesModal from './UserPreferencesModal.json';
|
||||
import ViewportDownloadForm from './ViewportDownloadForm.json';
|
||||
|
||||
export default {
|
||||
export default {
|
||||
'en-US': {
|
||||
AboutModal,
|
||||
Buttons,
|
||||
@ -21,5 +21,5 @@ export default {
|
||||
StudyList,
|
||||
UserPreferencesModal,
|
||||
ViewportDownloadForm,
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@ -6,8 +6,8 @@ import MeasurementTable from './MeasurementTable.json';
|
||||
import StudyList from './StudyList.json';
|
||||
import UserPreferencesModal from './UserPreferencesModal.json';
|
||||
|
||||
export default {
|
||||
'es': {
|
||||
export default {
|
||||
es: {
|
||||
Buttons,
|
||||
CineDialog,
|
||||
Common,
|
||||
@ -15,5 +15,5 @@ export default {
|
||||
MeasurementTable,
|
||||
StudyList,
|
||||
UserPreferencesModal,
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@ -4,12 +4,12 @@ import Common from './Common.json';
|
||||
import Header from './Header.json';
|
||||
import UserPreferencesModal from './UserPreferencesModal.json';
|
||||
|
||||
export default {
|
||||
'fr': {
|
||||
export default {
|
||||
fr: {
|
||||
Buttons,
|
||||
CineDialog,
|
||||
Common,
|
||||
Header,
|
||||
UserPreferencesModal,
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@ -4,12 +4,12 @@ import Common from './Common.json';
|
||||
import Header from './Header.json';
|
||||
import UserPreferencesModal from './UserPreferencesModal.json';
|
||||
|
||||
export default {
|
||||
export default {
|
||||
'ja-JP': {
|
||||
Buttons,
|
||||
CineDialog,
|
||||
Common,
|
||||
Header,
|
||||
UserPreferencesModal,
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@ -2,10 +2,10 @@ import Buttons from './Buttons.json';
|
||||
import Common from './Common.json';
|
||||
import Header from './Header.json';
|
||||
|
||||
export default {
|
||||
'nl': {
|
||||
export default {
|
||||
nl: {
|
||||
Buttons,
|
||||
Common,
|
||||
Header,
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@ -6,7 +6,7 @@ import DatePicker from './DatePicker.json';
|
||||
import Header from './Header.json';
|
||||
import UserPreferencesModal from './UserPreferencesModal.json';
|
||||
|
||||
export default {
|
||||
export default {
|
||||
'pt-BR': {
|
||||
AboutModal,
|
||||
Buttons,
|
||||
@ -15,5 +15,5 @@ export default {
|
||||
DatePicker,
|
||||
Header,
|
||||
UserPreferencesModal,
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@ -5,13 +5,13 @@ import Header from './Header.json';
|
||||
import StudyList from './StudyList.json';
|
||||
import UserPreferencesModal from './UserPreferencesModal.json';
|
||||
|
||||
export default {
|
||||
'vi': {
|
||||
export default {
|
||||
vi: {
|
||||
Buttons,
|
||||
CineDialog,
|
||||
Common,
|
||||
Header,
|
||||
StudyList,
|
||||
UserPreferencesModal,
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@ -6,8 +6,8 @@ import MeasurementTable from './MeasurementTable.json';
|
||||
import StudyList from './StudyList.json';
|
||||
import UserPreferencesModal from './UserPreferencesModal.json';
|
||||
|
||||
export default {
|
||||
'zh': {
|
||||
export default {
|
||||
zh: {
|
||||
Buttons,
|
||||
CineDialog,
|
||||
Common,
|
||||
@ -15,5 +15,5 @@ export default {
|
||||
MeasurementTable,
|
||||
StudyList,
|
||||
UserPreferencesModal,
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@ -1,46 +1,3 @@
|
||||
<<<<<<< HEAD
|
||||
# Basic Docz example
|
||||
|
||||
## Using `create-docz-app`
|
||||
|
||||
```sh
|
||||
npx create-docz-app docz-app-basic
|
||||
# or
|
||||
yarn create docz-app docz-app-basic
|
||||
```
|
||||
|
||||
## Download manually
|
||||
|
||||
```sh
|
||||
curl https://codeload.github.com/doczjs/docz/tar.gz/master | tar -xz --strip=2 docz-master/examples/basic
|
||||
mv basic docz-basic-example
|
||||
cd docz-basic-example
|
||||
```
|
||||
|
||||
## Setup
|
||||
|
||||
```sh
|
||||
yarn # npm i
|
||||
```
|
||||
|
||||
## Run
|
||||
|
||||
```sh
|
||||
yarn dev # npm run dev
|
||||
```
|
||||
|
||||
## Build
|
||||
|
||||
```sh
|
||||
yarn build # npm run build
|
||||
```
|
||||
|
||||
## Serve built app
|
||||
|
||||
```sh
|
||||
yarn serve # npm run serve
|
||||
```
|
||||
=======
|
||||
# @ohif/ui
|
||||
|
||||
React component library powered by `docz`
|
||||
@ -60,4 +17,3 @@ yarn build # npm run build
|
||||
# Serve (the "build" output)
|
||||
yarn serve # npm run serve
|
||||
```
|
||||
>>>>>>> 4a8675655... feat: nuke UI project; start new with updated tooling BREAKING_CHANGE (#1468)
|
||||
|
||||
@ -1,8 +1,11 @@
|
||||
const path = require('path');
|
||||
// ~~ Plugins
|
||||
const PnpWebpackPlugin = require(`pnp-webpack-plugin`); // Required until Webpack@5
|
||||
|
||||
exports.onCreateWebpackConfig = args => {
|
||||
args.actions.setWebpackConfig({
|
||||
resolve: {
|
||||
plugins: [PnpWebpackPlugin],
|
||||
// Note the '..' in the path because docz gatsby project lives in the '.docz' directory
|
||||
modules: [
|
||||
// monorepo root
|
||||
@ -16,5 +19,8 @@ exports.onCreateWebpackConfig = args => {
|
||||
// symlinks: true,
|
||||
// },
|
||||
},
|
||||
resolveLoader: {
|
||||
plugins: [PnpWebpackPlugin.moduleLoader(module)],
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@ -19,10 +19,12 @@
|
||||
],
|
||||
"scripts": {
|
||||
"start": "yarn run dev",
|
||||
"build:styles": "postcss src/styles/tailwind.css -o src/gatsby-theme-docz/app.css",
|
||||
"dev": "docz dev",
|
||||
"build": "docz build",
|
||||
"serve": "docz serve"
|
||||
"build:ui:ci": "docz build",
|
||||
"build:ui:deploy-preview": "docz build --base=/ui/",
|
||||
"dev": "docz dev",
|
||||
"serve": "docz serve",
|
||||
"docs:preview": "yarn run dev"
|
||||
},
|
||||
"dependencies": {
|
||||
"classnames": "^2.2.6",
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { useRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
|
||||
@ -6,17 +6,18 @@ const baseClasses =
|
||||
'leading-none font-sans text-center justify-center items-center outline-none transition duration-300 ease-in-out focus:outline-none';
|
||||
|
||||
const defaults = {
|
||||
variant: 'contained',
|
||||
color: 'default',
|
||||
size: 'medium',
|
||||
radius: 'medium',
|
||||
disabled: false,
|
||||
fullWidth: false,
|
||||
rounded: 'medium',
|
||||
size: 'medium',
|
||||
type: 'button',
|
||||
variant: 'contained',
|
||||
};
|
||||
|
||||
const radiusClasses = {
|
||||
const roundedClasses = {
|
||||
none: '',
|
||||
small: 'rounded-sm',
|
||||
small: 'rounded',
|
||||
medium: 'rounded-md',
|
||||
large: 'rounded-lg',
|
||||
full: 'rounded-full',
|
||||
@ -77,7 +78,7 @@ const Button = ({
|
||||
variant = defaults.variant,
|
||||
color = defaults.color,
|
||||
size = defaults.size,
|
||||
radius = defaults.radius,
|
||||
rounded = defaults.rounded,
|
||||
disabled = defaults.disabled,
|
||||
type = defaults.type,
|
||||
fullWidth = defaults.fullWidth,
|
||||
@ -86,14 +87,24 @@ const Button = ({
|
||||
className,
|
||||
...rest
|
||||
}) => {
|
||||
const baseClasses =
|
||||
'inline-flex items-center outline-none transition duration-300 ease-in-out font-bold focus:outline-none';
|
||||
|
||||
const startIcon = startIconProp && (
|
||||
<div className="mr-2">{startIconProp}</div>
|
||||
<div className="mr-2">
|
||||
{React.cloneElement(startIconProp, {
|
||||
className: classnames('w-4 h-4 fill-current'),
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
|
||||
const handleOnClick = e => {
|
||||
const endIcon = endIconProp && (
|
||||
<div className="ml-2">
|
||||
{React.cloneElement(endIconProp, {
|
||||
className: classnames('w-4 h-4 fill-current'),
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
const buttonElement = useRef(null);
|
||||
|
||||
const handleOnClick = (e) => {
|
||||
buttonElement.current.blur();
|
||||
if (rest.onClick) {
|
||||
rest.onClick(e);
|
||||
@ -106,11 +117,13 @@ const Button = ({
|
||||
className,
|
||||
baseClasses,
|
||||
variantClasses[variant][color],
|
||||
radiusClasses[radius],
|
||||
roundedClasses[rounded],
|
||||
sizeClasses[size],
|
||||
fullWidthClasses[fullWidth],
|
||||
disabledClasses[disabled]
|
||||
)}
|
||||
ref={buttonElement}
|
||||
onClick={handleOnClick}
|
||||
type={type}
|
||||
{...rest}
|
||||
>
|
||||
|
||||
@ -24,10 +24,10 @@ import { Button } from '@ohif/ui';
|
||||
<Button variant="outlined" className="m-1">
|
||||
Default
|
||||
</Button>
|
||||
<Button variant="contained" color="primary" className="m-1">
|
||||
<Button variant="outlined" color="primary" className="m-1">
|
||||
Primary
|
||||
</Button>
|
||||
<Button variant="contained" color="secondary" className="m-1">
|
||||
<Button variant="outlined" color="secondary" className="m-1">
|
||||
Secondary
|
||||
</Button>
|
||||
<Button variant="outlined" color="white" className="m-1">
|
||||
@ -36,17 +36,17 @@ import { Button } from '@ohif/ui';
|
||||
</div>
|
||||
</Playground>
|
||||
|
||||
## Outlined Buttons
|
||||
## Contained Buttons
|
||||
|
||||
<Playground>
|
||||
<div className="p-4">
|
||||
<Button variant="contained" className="m-1">
|
||||
Default
|
||||
</Button>
|
||||
<Button variant="outlined" color="primary" className="m-1">
|
||||
<Button variant="contained" color="primary" className="m-1">
|
||||
Primary
|
||||
</Button>
|
||||
<Button variant="outlined" color="secondary" className="m-1">
|
||||
<Button variant="contained" color="secondary" className="m-1">
|
||||
Secondary
|
||||
</Button>
|
||||
<Button variant="contained" color="white" className="m-1">
|
||||
@ -209,19 +209,19 @@ import { Button } from '@ohif/ui';
|
||||
<Playground>
|
||||
<div className="p-4">
|
||||
<div>
|
||||
<Button variant="text" size="small" className="m-1">
|
||||
<Button variant="contained" size="small" className="m-1">
|
||||
Small
|
||||
</Button>
|
||||
<Button variant="contained" size="medium" className="m-1">
|
||||
Medium
|
||||
</Button>
|
||||
<Button variant="outlined" size="large" className="m-1">
|
||||
<Button variant="contained" size="large" className="m-1">
|
||||
Large
|
||||
</Button>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
variant="text"
|
||||
variant="contained"
|
||||
size="small"
|
||||
color="primary"
|
||||
className="m-1"
|
||||
@ -237,39 +237,65 @@ import { Button } from '@ohif/ui';
|
||||
Medium
|
||||
</Button>
|
||||
<Button
|
||||
variant="outlined"
|
||||
variant="contained"
|
||||
size="large"
|
||||
color="primary"
|
||||
className="m-1"
|
||||
>
|
||||
large
|
||||
Large
|
||||
</Button>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
variant="text"
|
||||
size="small"
|
||||
color="secondary"
|
||||
className="m-1"
|
||||
>
|
||||
Small
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
size="medium"
|
||||
color="secondary"
|
||||
className="m-1"
|
||||
>
|
||||
Medium
|
||||
</Button>
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="large"
|
||||
color="secondary"
|
||||
className="m-1"
|
||||
>
|
||||
Large
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
size="small"
|
||||
color="secondary"
|
||||
className="m-1"
|
||||
>
|
||||
Small
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
size="medium"
|
||||
color="secondary"
|
||||
className="m-1"
|
||||
>
|
||||
Medium
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
size="large"
|
||||
color="secondary"
|
||||
className="m-1"
|
||||
>
|
||||
Large
|
||||
</Button>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
variant="contained"
|
||||
size="small"
|
||||
color="white"
|
||||
className="m-1"
|
||||
>
|
||||
Small
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
size="medium"
|
||||
color="white"
|
||||
className="m-1"
|
||||
>
|
||||
Medium
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
size="large"
|
||||
color="white"
|
||||
className="m-1"
|
||||
>
|
||||
Large
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@ -46,7 +46,7 @@ const renderYearsOptions = () => {
|
||||
return options;
|
||||
};
|
||||
|
||||
const DateRange = props => {
|
||||
const DateRange = (props) => {
|
||||
const { onChange, startDate, endDate } = props;
|
||||
const [focusedInput, setFocusedInput] = useState(null);
|
||||
const renderYearsOptionsCallback = useCallback(renderYearsOptions, []);
|
||||
@ -82,11 +82,11 @@ const DateRange = props => {
|
||||
onYearSelect: PropTypes.func,
|
||||
};
|
||||
|
||||
const handleMonthChange = event => {
|
||||
const handleMonthChange = (event) => {
|
||||
onMonthSelect(month, event.target.value);
|
||||
};
|
||||
|
||||
const handleYearChange = event => {
|
||||
const handleYearChange = (event) => {
|
||||
onYearSelect(month, event.target.value);
|
||||
};
|
||||
|
||||
@ -140,7 +140,7 @@ const DateRange = props => {
|
||||
})
|
||||
}}
|
||||
focusedInput={focusedInput}
|
||||
onFocusChange={updatedVal => setFocusedInput(updatedVal)}
|
||||
onFocusChange={(updatedVal) => setFocusedInput(updatedVal)}
|
||||
/** OPTIONAL */
|
||||
renderCalendarInfo={renderDatePresets}
|
||||
renderMonthElement={renderMonthElement}
|
||||
@ -150,7 +150,7 @@ const DateRange = props => {
|
||||
closeDatePicker: 'Close',
|
||||
clearDates: 'Clear dates',
|
||||
}}
|
||||
isOutsideRange={day => !isInclusivelyBeforeDay(day, moment())}
|
||||
isOutsideRange={(day) => !isInclusivelyBeforeDay(day, moment())}
|
||||
hideKeyboardShortcutsPanel={true}
|
||||
numberOfMonths={1}
|
||||
showClearDates={false}
|
||||
|
||||
@ -7,13 +7,13 @@ import { ButtonGroup, Button, StudyItem, ThumbnailList } from '@ohif/ui';
|
||||
const buttonClasses = 'text-white text-base border-none bg-black p-2 min-w-18';
|
||||
const activeButtonClasses = 'bg-primary-main';
|
||||
|
||||
const getInitialActiveTab = tabs => {
|
||||
const getInitialActiveTab = (tabs) => {
|
||||
return tabs && tabs[0] && tabs[0].name;
|
||||
};
|
||||
|
||||
const getTrackedSeries = displaySets => {
|
||||
const getTrackedSeries = (displaySets) => {
|
||||
let trackedSeries = 0;
|
||||
displaySets.forEach(displaySet => {
|
||||
displaySets.forEach((displaySet) => {
|
||||
if (displaySet.isTracked) {
|
||||
trackedSeries++;
|
||||
}
|
||||
@ -25,9 +25,10 @@ const getTrackedSeries = displaySets => {
|
||||
const StudyBrowser = ({ tabs, onClickStudy, onClickThumbnail }) => {
|
||||
const [tabActive, setTabActive] = useState(getInitialActiveTab(tabs));
|
||||
const [studyActive, setStudyActive] = useState(null);
|
||||
const [thumbnailActive, setThumbnailActive] = useState(null);
|
||||
|
||||
const getTabContent = () => {
|
||||
const tabData = tabs.find(tab => tab.name === tabActive);
|
||||
const tabData = tabs.find((tab) => tab.name === tabActive);
|
||||
|
||||
if (!tabData || !tabData.studies || !Array.isArray(tabData.studies)) {
|
||||
return;
|
||||
@ -36,9 +37,9 @@ const StudyBrowser = ({ tabs, onClickStudy, onClickThumbnail }) => {
|
||||
return tabData.studies.map(
|
||||
({
|
||||
studyInstanceUid,
|
||||
studyDate,
|
||||
studyDescription,
|
||||
instances,
|
||||
date,
|
||||
description,
|
||||
numInstances,
|
||||
modalities,
|
||||
displaySets,
|
||||
}) => {
|
||||
@ -46,9 +47,9 @@ const StudyBrowser = ({ tabs, onClickStudy, onClickThumbnail }) => {
|
||||
return (
|
||||
<React.Fragment key={studyInstanceUid}>
|
||||
<StudyItem
|
||||
studyDate={studyDate}
|
||||
studyDescription={studyDescription}
|
||||
instances={instances}
|
||||
date={date}
|
||||
description={description}
|
||||
numInstances={numInstances}
|
||||
modalities={modalities}
|
||||
trackedSeries={getTrackedSeries(displaySets)}
|
||||
isActive={isActive}
|
||||
@ -64,16 +65,17 @@ const StudyBrowser = ({ tabs, onClickStudy, onClickThumbnail }) => {
|
||||
<ThumbnailList
|
||||
thumbnails={displaySets}
|
||||
thumbnailActive={thumbnailActive}
|
||||
onThumbnailClick={thumbnailId => {
|
||||
setThumbnailActive(
|
||||
thumbnailId === thumbnailActive ? null : thumbnailId
|
||||
);
|
||||
onThumbnailClick={(thumbnailId) => {
|
||||
setThumbnailActive(
|
||||
thumbnailId === thumbnailActive ? null : thumbnailId
|
||||
)
|
||||
|
||||
if (onClickThumbnail) {
|
||||
// TODO: what is thumbnailId? Should pass display set instead
|
||||
onClickThumbnail(thumbnailId);
|
||||
if (onClickThumbnail) {
|
||||
// TODO: what is thumbnailId? Should pass display set instead
|
||||
onClickThumbnail(thumbnailId);
|
||||
}
|
||||
}
|
||||
}}
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</React.Fragment>
|
||||
@ -90,7 +92,7 @@ const StudyBrowser = ({ tabs, onClickStudy, onClickThumbnail }) => {
|
||||
color="inherit"
|
||||
className="border border-secondary-light rounded-md"
|
||||
>
|
||||
{tabs.map(tab => {
|
||||
{tabs.map((tab) => {
|
||||
const { name, label } = tab;
|
||||
const isActive = tabActive === name;
|
||||
return (
|
||||
@ -131,7 +133,7 @@ StudyBrowser.propTypes = {
|
||||
studyInstanceUid: PropTypes.string.isRequired,
|
||||
date: PropTypes.string,
|
||||
numInstances: PropTypes.number,
|
||||
modality: PropTypes.string,
|
||||
modalities: PropTypes.string,
|
||||
description: PropTypes.string,
|
||||
displaySets: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
|
||||
@ -18,7 +18,88 @@ import { StudyBrowser } from '@ohif/ui';
|
||||
## Basic usage
|
||||
|
||||
<Playground>
|
||||
<StudyBrowser />
|
||||
{() => {
|
||||
const studyWithSR = {
|
||||
studyInstanceUid: '1',
|
||||
date: '07-Sept-2010',
|
||||
description: 'CHEST/ABD/PELVIS W/CONTRAST',
|
||||
numInstances: 902,
|
||||
modalities: 'CT,SR',
|
||||
displaySets: [
|
||||
{
|
||||
displaySetInstanceUid: 'f69f6asdasd48c-223e-db7f-c4af-b8906641a66e',
|
||||
description: 'Multiple line image series description lorem sit',
|
||||
seriesNumber: 1,
|
||||
numInstances: 68,
|
||||
componentType: 'thumbnailTracked',
|
||||
viewportIdentificator: 'A',
|
||||
isTracked: true,
|
||||
},
|
||||
{
|
||||
displaySetInstanceUid: 'f69f648c-223e-db7f-c4asdasdaf-b8906641a66e',
|
||||
description: 'Multiple line image series description lorem sit',
|
||||
seriesNumber: 1,
|
||||
numInstances: 68,
|
||||
componentType: 'thumbnailTracked',
|
||||
viewportIdentificator: 'B',
|
||||
},
|
||||
{
|
||||
displaySetInstanceUid: 'f69f648c-223e-dasdasdb7f-c4af-b8906641a66e',
|
||||
description: 'Multiple line image series description lorem sit',
|
||||
seriesNumber: 1,
|
||||
numInstances: 68,
|
||||
componentType: 'thumbnailTracked',
|
||||
},
|
||||
{
|
||||
displaySetInstanceUid: 'f69f648c-223e-db7f-c4afas-b8906asd641a66e',
|
||||
description: 'Multiple line description lorem ipsum dolor sit amet',
|
||||
modality: 'SR',
|
||||
componentType: 'thumbnailNoImage',
|
||||
seriesDate: '07-Sept-2010',
|
||||
},
|
||||
],
|
||||
};
|
||||
const studySimple = {
|
||||
studyInstanceUid: '2',
|
||||
date: '07-Sept-2010',
|
||||
description: 'CHEST/ABD/PELVIS W/CONTRAST',
|
||||
numInstances: 902,
|
||||
modalities: 'CT',
|
||||
displaySets: [
|
||||
{
|
||||
displaySetInstanceUid: 'f69f648c-223e-db7f-c4af-b8906641a66e',
|
||||
description: 'Multiple line image series description lorem sit',
|
||||
seriesNumber: 1,
|
||||
numInstances: 68,
|
||||
componentType: 'thumbnailTracked',
|
||||
},
|
||||
],
|
||||
};
|
||||
const tabs = [
|
||||
{
|
||||
name: 'primary',
|
||||
label: 'Primary',
|
||||
studies: [studySimple],
|
||||
},
|
||||
{
|
||||
name: 'recent',
|
||||
label: 'Recent',
|
||||
studies: [studyWithSR, studySimple],
|
||||
},
|
||||
{
|
||||
name: 'all',
|
||||
label: 'All',
|
||||
studies: [studySimple, studyWithSR],
|
||||
},
|
||||
];
|
||||
return (
|
||||
<div className="flex flex-1" style={{height: '400px'}}>
|
||||
<div className="overflow-hidden w-64">
|
||||
<StudyBrowser tabs={tabs} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</Playground>
|
||||
|
||||
## Properties
|
||||
|
||||
@ -8,9 +8,9 @@ const baseClasses =
|
||||
'first:border-0 border-t border-secondary-light cursor-pointer outline-none';
|
||||
|
||||
const StudyItem = ({
|
||||
studyDate,
|
||||
studyDescription,
|
||||
instances,
|
||||
date,
|
||||
description,
|
||||
numInstances,
|
||||
modalities,
|
||||
trackedSeries,
|
||||
isActive,
|
||||
@ -29,15 +29,15 @@ const StudyItem = ({
|
||||
>
|
||||
<div className="flex flex-col flex-1 px-4 pb-2">
|
||||
<div className="flex flex-row items-center justify-between pb-2 pt-2 pb-2">
|
||||
<div className="text-white text-base">{studyDate}</div>
|
||||
<div className="text-white text-base">{date}</div>
|
||||
<div className="flex flex-row items-center text-blue-300 text-base">
|
||||
<Icon name="group-layers" className="text-blue-300 mx-2 w-4" />
|
||||
{instances}
|
||||
{numInstances}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-row py-1">
|
||||
<div className="text-blue-300 pr-5 text-xl">{modalities}</div>
|
||||
<div className="text-blue-300 text-base">{studyDescription}</div>
|
||||
<div className="text-blue-300 text-base">{description}</div>
|
||||
</div>
|
||||
</div>
|
||||
{!!trackedSeries && (
|
||||
|
||||
@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
|
||||
|
||||
import { Thumbnail, ThumbnailNoImage, ThumbnailTracked } from '@ohif/ui';
|
||||
|
||||
const ThumbnailList = ({ thumbnails }) => {
|
||||
const ThumbnailList = ({ thumbnails, thumbnailActive, onThumbnailClick }) => {
|
||||
return (
|
||||
<div className="bg-black py-3">
|
||||
{thumbnails.map(
|
||||
@ -17,10 +17,11 @@ const ThumbnailList = ({ thumbnails }) => {
|
||||
seriesDate,
|
||||
viewportIdentificator,
|
||||
isTracked,
|
||||
isActive,
|
||||
imageSrc,
|
||||
imageAltText,
|
||||
}) => {
|
||||
const isActive = thumbnailActive === displaySetInstanceUid;
|
||||
|
||||
switch (componentType) {
|
||||
case 'thumbnail':
|
||||
return (
|
||||
@ -33,7 +34,7 @@ const ThumbnailList = ({ thumbnails }) => {
|
||||
imageAltText={imageAltText}
|
||||
viewportIdentificator={viewportIdentificator}
|
||||
isActive={isActive}
|
||||
onClick={() => {}}
|
||||
onClick={() => onThumbnailClick(displaySetInstanceUid)}
|
||||
/>
|
||||
);
|
||||
case 'thumbnailNoImage':
|
||||
@ -43,7 +44,7 @@ const ThumbnailList = ({ thumbnails }) => {
|
||||
modality={modality}
|
||||
seriesDate={seriesDate}
|
||||
description={description}
|
||||
onClick={() => {}}
|
||||
onClick={() => onThumbnailClick(displaySetInstanceUid)}
|
||||
/>
|
||||
);
|
||||
case 'thumbnailTracked':
|
||||
@ -58,7 +59,7 @@ const ThumbnailList = ({ thumbnails }) => {
|
||||
viewportIdentificator={viewportIdentificator}
|
||||
isTracked={isTracked}
|
||||
isActive={isActive}
|
||||
onClick={() => {}}
|
||||
onClick={() => onThumbnailClick(displaySetInstanceUid)}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
@ -87,9 +88,10 @@ ThumbnailList.propTypes = {
|
||||
]).isRequired,
|
||||
viewportIdentificator: PropTypes.string,
|
||||
isTracked: PropTypes.bool,
|
||||
isActive: PropTypes.bool,
|
||||
})
|
||||
),
|
||||
thumbnailActive: PropTypes.string,
|
||||
onThumbnailClick: PropTypes.func,
|
||||
};
|
||||
|
||||
export default ThumbnailList;
|
||||
|
||||
@ -8,18 +8,19 @@ const ThumbnailNoImage = ({ description, seriesDate, modality, onClick }) => {
|
||||
<div
|
||||
className="flex flex-row flex-1 px-4 py-3 cursor-pointer"
|
||||
onClick={onClick}
|
||||
onKeyDown={onClick}
|
||||
role="button"
|
||||
tabIndex="0"
|
||||
>
|
||||
<div className="flex flex-2 pl-2 pr-3">
|
||||
<Icon name="list-bullets" className="text-secondary-light" />
|
||||
</div>
|
||||
<div className="flex flex-col flex-1">
|
||||
<div className="flex flex-row flex-1 items-center mb-2">
|
||||
<div className="px-4 bg-primary-main rounded-sm mr-4 text-xl text-white">
|
||||
<Icon name="list-bullets" className="text-secondary-light w-12" />
|
||||
<div className="px-3 bg-primary-main rounded-sm mr-4 text-lg text-white">
|
||||
{modality}
|
||||
</div>
|
||||
<span className="text-blue-300 text-base">{seriesDate}</span>
|
||||
</div>
|
||||
<div className="text-white text-base">{description}</div>
|
||||
<div className="text-white text-base ml-12">{description}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -21,14 +21,12 @@ import { Thumbnail } from '@ohif/ui';
|
||||
|
||||
<Playground>
|
||||
<div className="flex flex-1 items-center justify-center p-4">
|
||||
<div className="w-56">
|
||||
<Thumbnail
|
||||
imageAltText="DisplaySet thumbnail image"
|
||||
<div className="w-64">
|
||||
<ThumbnailNoImage
|
||||
modality={'SR'}
|
||||
seriesDate="13-Mar-1999"
|
||||
description="Multiple line image series description lorem sit"
|
||||
seriesNumber={4}
|
||||
numInstances={902}
|
||||
onClick={() => alert('Thumbnail was clicked!')}
|
||||
isActive={false}
|
||||
onClick={() => alert('ThumbnailNoImage was clicked!')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -36,4 +34,4 @@ import { Thumbnail } from '@ohif/ui';
|
||||
|
||||
## Properties
|
||||
|
||||
<Props of={Thumbnail} />
|
||||
<Props of={ThumbnailNoImage} />
|
||||
|
||||
@ -1,26 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import './ContextMenu.css';
|
||||
|
||||
const ContextMenu = ({ items, onClick }) => {
|
||||
return (
|
||||
<div className="ContextMenu" onContextMenu={e => e.preventDefault()}>
|
||||
<ul>
|
||||
{items.map((item, index) => (
|
||||
<li key={index}>
|
||||
<button className="form-action" onClick={() => onClick(item)}>
|
||||
<span key={index}>{item.label}</span>
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ContextMenu.propTypes = {
|
||||
items: PropTypes.array.isRequired,
|
||||
onClick: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default ContextMenu;
|
||||
@ -1,129 +0,0 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import './LayoutChooser.styl';
|
||||
|
||||
/**
|
||||
* Adds the 'hover' class to cells above and to the left of the current cell
|
||||
* This is used to "fill in" the grid that the user will change the layout to,
|
||||
* if they click on a specific table cell.
|
||||
**/
|
||||
|
||||
class LayoutChooser extends PureComponent {
|
||||
static propTypes = {
|
||||
Rows: PropTypes.number.isRequired,
|
||||
Columns: PropTypes.number.isRequired,
|
||||
visible: PropTypes.bool.isRequired,
|
||||
selectedCell: PropTypes.object,
|
||||
boxSize: PropTypes.number.isRequired,
|
||||
cellBorder: PropTypes.number.isRequired,
|
||||
onClick: PropTypes.func,
|
||||
onChange: PropTypes.func,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
Rows: 3,
|
||||
Columns: 3,
|
||||
visible: true,
|
||||
boxSize: 20,
|
||||
cellBorder: 1,
|
||||
selectedCell: {
|
||||
row: -1,
|
||||
col: -1,
|
||||
},
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.emptyCell = {
|
||||
row: -1,
|
||||
column: -1,
|
||||
};
|
||||
this.state = {
|
||||
table: [[]],
|
||||
selectedCell: this.props.selectedCell,
|
||||
};
|
||||
}
|
||||
componentDidMount() {
|
||||
this.highlightCells(this.emptyCell);
|
||||
}
|
||||
onClick(currentCell) {
|
||||
this.setState({
|
||||
selectedCell: currentCell,
|
||||
});
|
||||
this.highlightCells(currentCell);
|
||||
if (this.props.onClick) {
|
||||
this.props.onClick(currentCell);
|
||||
}
|
||||
if (this.props.onChange) {
|
||||
this.props.onChange(currentCell);
|
||||
}
|
||||
}
|
||||
isRange = (cell, parentCell) => {
|
||||
return cell.row <= parentCell.row && cell.col <= parentCell.col;
|
||||
};
|
||||
highlightCells = currentCell => {
|
||||
let table = [];
|
||||
for (let row = 0; row < this.props.Rows; row++) {
|
||||
let newRow = [];
|
||||
for (let col = 0; col < this.props.Columns; col++) {
|
||||
let cell = { row: row, col: col };
|
||||
if (this.isRange(cell, currentCell)) {
|
||||
cell.className = 'hover';
|
||||
} else if (
|
||||
this.isRange(currentCell, this.emptyCell) &&
|
||||
this.isRange(cell, this.state.selectedCell)
|
||||
) {
|
||||
cell.className = 'selectedBefore';
|
||||
}
|
||||
newRow.push(cell);
|
||||
}
|
||||
table.push(newRow);
|
||||
}
|
||||
this.setState({ table: table });
|
||||
};
|
||||
|
||||
render() {
|
||||
let Columns = this.props.Columns;
|
||||
const style = {
|
||||
display: this.props.visible ? 'block' : 'none',
|
||||
minWidth:
|
||||
Columns * this.props.boxSize + (Columns + 5) * this.props.cellBorder,
|
||||
};
|
||||
return (
|
||||
<div
|
||||
className="layoutChooser layoutChooser-dropdown-menu"
|
||||
role="menu"
|
||||
style={style}
|
||||
>
|
||||
<table>
|
||||
<tbody>
|
||||
{this.state.table.map((row, i) => {
|
||||
return (
|
||||
<tr key={i}>
|
||||
{row.map((cell, j) => {
|
||||
return (
|
||||
<td
|
||||
className={cell.className}
|
||||
style={{
|
||||
width: this.props.boxSize,
|
||||
height: this.props.boxSize,
|
||||
border: 'solid 1px black',
|
||||
}}
|
||||
key={j}
|
||||
onMouseEnter={() => this.highlightCells(cell)}
|
||||
onMouseLeave={() => this.highlightCells(this.emptyCell)}
|
||||
onClick={() => this.onClick(cell)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export { LayoutChooser };
|
||||
@ -1,65 +0,0 @@
|
||||
.measurementItem
|
||||
.rowActions
|
||||
margin-left: -1px;
|
||||
background-color: var(--ui-gray-darker)
|
||||
height: 0
|
||||
overflow: hidden
|
||||
transition: all 0.3s ease
|
||||
visibility: hidden
|
||||
padding-left: 14px
|
||||
|
||||
.btnAction
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
color: var(--default-color)
|
||||
cursor: pointer
|
||||
line-height: 35px
|
||||
height: 35px
|
||||
transition: all 0.3s ease
|
||||
|
||||
&:hover, &:active
|
||||
color: var(--text-primary-color)
|
||||
|
||||
i
|
||||
margin-right: 4px;
|
||||
|
||||
&.selected
|
||||
.rowActions
|
||||
height: auto;
|
||||
visibility: visible
|
||||
|
||||
.measurementLocation
|
||||
margin-top: 9px;
|
||||
overflow: hidden;
|
||||
max-width: calc(100% - 50px);
|
||||
text-overflow: ellipsis;
|
||||
overflow-wrap: normal;
|
||||
white-space: nowrap;
|
||||
|
||||
.measurementDisplayText
|
||||
display: inline-block;
|
||||
margin-top: 9px;
|
||||
padding-left: 9px;
|
||||
width: 90px;
|
||||
border-left: 1px solid var(--text-secondary-color);
|
||||
color: var(--text-primary-color);
|
||||
|
||||
.itemIndex
|
||||
-webkit-box-sizing: initial;
|
||||
-moz-box-sizing: initial;
|
||||
box-sizing: initial;
|
||||
|
||||
&.hasWarnings
|
||||
.itemIndex
|
||||
opacity: 1
|
||||
background-color: #e29e4a;
|
||||
color: #fff;
|
||||
|
||||
.warning-icon
|
||||
display: block;
|
||||
margin: 7px auto 0;
|
||||
|
||||
svg
|
||||
width: 22px;
|
||||
height: 20px;
|
||||
pointer-events: inherit;
|
||||
@ -1,151 +0,0 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { StudiesList } from './StudiesList.js';
|
||||
import { ScrollableArea } from './../../ScrollableArea/ScrollableArea.js';
|
||||
import { SeriesList } from './SeriesList.js';
|
||||
|
||||
import './QuickSwitch.styl';
|
||||
|
||||
export class QuickSwitch extends Component {
|
||||
static propTypes = {
|
||||
side: PropTypes.string,
|
||||
studyListData: PropTypes.array.isRequired,
|
||||
onSeriesSelected: PropTypes.func.isRequired,
|
||||
seriesData: PropTypes.array,
|
||||
onStudySelected: PropTypes.func,
|
||||
activeStudyInstanceUID: PropTypes.string,
|
||||
activeDisplaySetInstanceUID: PropTypes.string,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
seriesQuickSwitchOpen: false,
|
||||
sideClass: this.props.side || '',
|
||||
activeStudyInstanceUID: this.props.activeStudyInstanceUID,
|
||||
activeDisplaySetInstanceUID: this.props.activeDisplaySetInstanceUID,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const props = this.props;
|
||||
|
||||
if (props.activeStudyInstanceUID !== prevProps.activeStudyInstanceUID) {
|
||||
this.setState({
|
||||
activeStudyInstanceUID: props.activeStudyInstanceUID,
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
props.activeDisplaySetInstanceUID !==
|
||||
prevProps.activeDisplaySetInstanceUID
|
||||
) {
|
||||
this.setState({
|
||||
activeDisplaySetInstanceUID: props.activeDisplaySetInstanceUID,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const quickSwitchClass = this.state.seriesQuickSwitchOpen
|
||||
? 'series-triggered'
|
||||
: '';
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`series-quick-switch clearfix noselect ${this.state.sideClass} ${quickSwitchClass}`}
|
||||
onMouseLeave={this.hideSeriesSwitch}
|
||||
>
|
||||
<div className="series-switch" onMouseEnter={this.showSeriesSwitch}>
|
||||
<div className="title-label">Series</div>
|
||||
<div className="series-box">
|
||||
{this.getSmallListItems()}
|
||||
<ScrollableArea scrollStep={201} class="series-browser">
|
||||
<SeriesList
|
||||
seriesItems={this.getSeriesItems()}
|
||||
onClick={this.onSeriesClick}
|
||||
activeDisplaySetInstanceUID={
|
||||
this.state.activeDisplaySetInstanceUID
|
||||
}
|
||||
/>
|
||||
</ScrollableArea>
|
||||
</div>
|
||||
</div>
|
||||
<div className="study-switch">
|
||||
<div className="title-label">Study</div>
|
||||
<div className="study-box">
|
||||
<ScrollableArea scrollStep={91} class="study-browser">
|
||||
<StudiesList
|
||||
studyListData={this.props.studyListData}
|
||||
onClick={this.onStudyClick}
|
||||
activeStudyInstanceUID={this.state.activeStudyInstanceUID}
|
||||
/>
|
||||
</ScrollableArea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
getSeriesItems = () => {
|
||||
let seriesData;
|
||||
|
||||
if (this.props.seriesData) {
|
||||
seriesData = this.props.seriesData;
|
||||
} else if (this.state.activeStudyInstanceUID) {
|
||||
const study = this.props.studyListData.find(
|
||||
study => study.StudyInstanceUID === this.state.activeStudyInstanceUID
|
||||
);
|
||||
|
||||
seriesData = study.thumbnails;
|
||||
} else {
|
||||
seriesData = this.props.studyListData[0].thumbnails;
|
||||
}
|
||||
|
||||
return seriesData || [];
|
||||
};
|
||||
|
||||
getSmallListItems = () => {
|
||||
const seriesItems = this.getSeriesItems() || [];
|
||||
return seriesItems.map((seriesData, index) => {
|
||||
const active =
|
||||
seriesData.displaySetInstanceUID ===
|
||||
this.state.activeDisplaySetInstanceUID;
|
||||
return (
|
||||
<div key={index} className={`series-item ${active ? 'active' : ''}`} />
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
onStudyClick = studyDataSelected => {
|
||||
if (this.props.onStudySelected) {
|
||||
this.props.onStudySelected(studyDataSelected);
|
||||
}
|
||||
this.setState({
|
||||
activeStudyInstanceUID: studyDataSelected.StudyInstanceUID,
|
||||
seriesQuickSwitchOpen: true,
|
||||
});
|
||||
};
|
||||
|
||||
onSeriesClick = seriesDataSelected => {
|
||||
this.setState({
|
||||
activeDisplaySetInstanceUID: seriesDataSelected.displaySetInstanceUID,
|
||||
});
|
||||
|
||||
this.props.onSeriesSelected(seriesDataSelected);
|
||||
};
|
||||
|
||||
showSeriesSwitch = () => {
|
||||
this.setState({
|
||||
seriesQuickSwitchOpen: true,
|
||||
});
|
||||
};
|
||||
|
||||
hideSeriesSwitch = () => {
|
||||
this.setState({
|
||||
seriesQuickSwitchOpen: false,
|
||||
});
|
||||
};
|
||||
}
|
||||
@ -1,40 +0,0 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { Thumbnail } from './../studyBrowser';
|
||||
import './SeriesList.styl';
|
||||
|
||||
export class SeriesList extends Component {
|
||||
static propTypes = {
|
||||
seriesItems: PropTypes.array.isRequired,
|
||||
onClick: PropTypes.func.isRequired,
|
||||
activeDisplaySetInstanceUID: PropTypes.string,
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div className="study-browser-series clearfix thumbnails-wrapper">
|
||||
<div className="study-series-container">{this.getSeriesItems()}</div>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
getSeriesItems = () => {
|
||||
return this.props.seriesItems.map((seriesData, index) => {
|
||||
return (
|
||||
<Thumbnail
|
||||
key={seriesData.displaySetInstanceUID}
|
||||
id={`series_thumb_${index}`}
|
||||
{...seriesData}
|
||||
active={
|
||||
seriesData.displaySetInstanceUID ===
|
||||
this.props.activeDisplaySetInstanceUID
|
||||
}
|
||||
onClick={() => this.props.onClick(seriesData)}
|
||||
/>
|
||||
);
|
||||
});
|
||||
};
|
||||
}
|
||||
@ -1,60 +0,0 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import './StudiesItem.styl';
|
||||
|
||||
export class StudiesItem extends Component {
|
||||
static propTypes = {
|
||||
onClick: PropTypes.func.isRequired,
|
||||
studyData: PropTypes.object.isRequired,
|
||||
active: PropTypes.bool,
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
StudyDate,
|
||||
StudyDescription,
|
||||
modalities,
|
||||
studyAvailable,
|
||||
} = this.props.studyData;
|
||||
const activeClass = this.props.active ? ' active' : '';
|
||||
const hasDescriptionAndDate = StudyDate && StudyDescription;
|
||||
return (
|
||||
<div
|
||||
className={`studyBrowseItem${activeClass}`}
|
||||
onClick={this.props.onClick}
|
||||
>
|
||||
<div className="studyItemBox">
|
||||
<div className="studyModality">
|
||||
<div
|
||||
className="studyModalityText"
|
||||
style={this.getModalitiesStyle()}
|
||||
>
|
||||
{modalities}
|
||||
</div>
|
||||
</div>
|
||||
<div className="studyText">
|
||||
{hasDescriptionAndDate ? (
|
||||
<React.Fragment>
|
||||
<div className="studyDate">{StudyDate}</div>
|
||||
<div className="studyDescription">{StudyDescription}</div>
|
||||
</React.Fragment>
|
||||
) : (
|
||||
<div className="studyAvailability">
|
||||
{studyAvailable ? (
|
||||
<React.Fragment>N/A</React.Fragment>
|
||||
) : (
|
||||
<React.Fragment>Click to load</React.Fragment>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
getModalitiesStyle = () => {
|
||||
return {};
|
||||
};
|
||||
}
|
||||
@ -1,133 +0,0 @@
|
||||
$boxHoverBackgroundColor = #14191E
|
||||
$boxHoverBorderColor = #2d4660
|
||||
$boxActiveBorderColor = #131D29
|
||||
$boxWidth = 50px
|
||||
$borderThickness = 2px
|
||||
$boxBorderRadius = 8px
|
||||
$nestingMargin = 6px
|
||||
$spacerX = 7px
|
||||
$spacerY = 12px
|
||||
|
||||
.studyBrowseItem
|
||||
margin-bottom: 10px;
|
||||
position: relative
|
||||
// required transformation to make inner fixed elements relative to this one
|
||||
transform: scale(1)
|
||||
|
||||
&.active
|
||||
.studyItemBox
|
||||
border-color: var(--active-color)
|
||||
|
||||
.studyModality
|
||||
color: var(--primary-background-color)
|
||||
&
|
||||
&:before
|
||||
&:after
|
||||
background-color: var(--active-color)
|
||||
border-color: $boxActiveBorderColor
|
||||
|
||||
&:not(.active) .studyBrowserSeries
|
||||
max-height: 0 !important
|
||||
|
||||
.studySeriesContainer
|
||||
opacity: 0
|
||||
transform: translateY(-100%)
|
||||
|
||||
.studyBrowserSeries
|
||||
overflow: hidden
|
||||
transition: max-height 0.3s ease
|
||||
|
||||
.studySeriesContainer
|
||||
opacity: 1
|
||||
transition: opacity 0.3s ease, transform 0.3s ease
|
||||
transform: translateY(0)
|
||||
transform-origin: 50% 0%
|
||||
|
||||
.studyItemBox
|
||||
border: 3px solid var(--ui-border-color-dark)
|
||||
border-radius: 12px
|
||||
cursor: pointer
|
||||
padding: $spacerY $spacerX ($spacerY - 1)
|
||||
position: relative
|
||||
transition: $sidebarTransition
|
||||
background-color: $boxHoverBackgroundColor
|
||||
z-index: 0
|
||||
|
||||
&:hover
|
||||
border-color: var(--active-color)
|
||||
|
||||
&.additional
|
||||
color: var(--text-secondary-color)
|
||||
|
||||
.studyModality
|
||||
color: var(--primary-background-color)
|
||||
&
|
||||
&:before
|
||||
&:after
|
||||
background-color: var(--active-color)
|
||||
|
||||
&.additional
|
||||
color: var(--text-primary-color)
|
||||
font-size: 16px
|
||||
font-weight: normal
|
||||
height: 91px
|
||||
line-height: 91px
|
||||
padding: 0
|
||||
text-align: center
|
||||
|
||||
.studyText
|
||||
font-size: 13px
|
||||
left: ($spacerX * 3) + $boxWidth + ($nestingMargin * 3)
|
||||
line-height: 14px
|
||||
position: absolute
|
||||
right: $spacerX
|
||||
top: $spacerY
|
||||
.studyDate
|
||||
margin-top: 8px
|
||||
color: var(--text-secondary-color)
|
||||
.studyDescription
|
||||
margin-top: 8px
|
||||
color: var(--text-primary-color)
|
||||
.studyAvailability
|
||||
margin-top: 24px
|
||||
color: var(--text-primary-color)
|
||||
|
||||
.studyModality
|
||||
color: var(--text-secondary-color)
|
||||
font-size: 20px
|
||||
line-height: $boxWidth
|
||||
margin-left: $nestingMargin * 2
|
||||
margin-top: $nestingMargin * 2
|
||||
position: relative
|
||||
|
||||
.studyModalityText
|
||||
height: 100%
|
||||
overflow: hidden
|
||||
text-align: center
|
||||
text-transform: uppercase
|
||||
|
||||
&
|
||||
&:before
|
||||
&:after
|
||||
border: $borderThickness solid var(--primary-background-color)
|
||||
background-color: var(--box-background-color)
|
||||
border-radius: $boxBorderRadius
|
||||
height: $boxWidth + ($borderThickness * 2)
|
||||
transition($sidebarTransition)
|
||||
width: $boxWidth + ($borderThickness * 2)
|
||||
|
||||
&:before
|
||||
&:after
|
||||
display: block
|
||||
content: ''
|
||||
position: absolute
|
||||
|
||||
&:before
|
||||
top: -($borderThickness + $nestingMargin)
|
||||
left: -($borderThickness + $nestingMargin)
|
||||
z-index: -1
|
||||
|
||||
&:after
|
||||
top: -($borderThickness + ($nestingMargin * 2))
|
||||
left: -($borderThickness + ($nestingMargin * 2))
|
||||
z-index: -2
|
||||
@ -1,37 +0,0 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { StudiesItem } from './StudiesItem.js';
|
||||
import './StudiesList.styl';
|
||||
|
||||
export class StudiesList extends Component {
|
||||
static propTypes = {
|
||||
class: PropTypes.string,
|
||||
studyListData: PropTypes.array.isRequired,
|
||||
onClick: PropTypes.func.isRequired,
|
||||
activeStudyInstanceUID: PropTypes.string,
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className={`studiesList ${this.props.class}`}>
|
||||
{this.getBrowserItems()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
getBrowserItems = () => {
|
||||
return this.props.studyListData.map((studyData, index) => {
|
||||
return (
|
||||
<StudiesItem
|
||||
key={index}
|
||||
studyData={studyData}
|
||||
active={
|
||||
studyData.StudyInstanceUID === this.props.activeStudyInstanceUID
|
||||
}
|
||||
onClick={event => this.props.onClick(studyData)}
|
||||
/>
|
||||
);
|
||||
});
|
||||
};
|
||||
}
|
||||
@ -1,61 +0,0 @@
|
||||
---
|
||||
name: Quick Switch
|
||||
menu: Components
|
||||
route: /components/quick-switch
|
||||
---
|
||||
|
||||
import { Playground, Props } from 'docz'
|
||||
import { State } from 'react-powerplug'
|
||||
import { QuickSwitch } from './../index.js'
|
||||
//
|
||||
import studies from './studies.js'
|
||||
|
||||
# Quick Switch
|
||||
|
||||
## Basic usage
|
||||
|
||||
<Playground>
|
||||
<State initial={{
|
||||
studyListData: studies,
|
||||
}}>
|
||||
{({ state, setState }) => (
|
||||
<QuickSwitch
|
||||
studyListData={state.studyListData}
|
||||
// If this is always activeStudy's thumbnails,
|
||||
// Why do we need to specify this?
|
||||
seriesData={
|
||||
state.studyListData.find((study) => {
|
||||
return !!study.active;
|
||||
}).thumbnails}
|
||||
onSeriesSelected={(selectedSeries) => {
|
||||
const { studyListData } = state;
|
||||
const selectedId = selectedSeries.displaySetInstanceUID;
|
||||
|
||||
studyListData.forEach(study => {
|
||||
study.thumbnails.forEach(series => {
|
||||
series.active = series.displaySetInstanceUID === selectedId;
|
||||
})
|
||||
});
|
||||
|
||||
setState({ studyListData });
|
||||
}}
|
||||
onStudySelected={(selectedStudy) => {
|
||||
const { studyListData } = state;
|
||||
const selectedId = selectedStudy.StudyInstanceUID;
|
||||
|
||||
studyListData.forEach(study => {
|
||||
study.active = study.StudyInstanceUID === selectedId;
|
||||
});
|
||||
|
||||
setState({ studyListData });
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
</State>
|
||||
|
||||
</Playground>
|
||||
|
||||
## API
|
||||
|
||||
<Props of={QuickSwitch} />
|
||||
@ -1,76 +0,0 @@
|
||||
export default [
|
||||
{
|
||||
StudyInstanceUID: '10001',
|
||||
StudyDescription: 'Anti-PD-1',
|
||||
modalities: 'CT',
|
||||
StudyDate: '18-nov-2018',
|
||||
active: false,
|
||||
thumbnails: [
|
||||
{
|
||||
imageSrc:
|
||||
'https://raw.githubusercontent.com/crowds-cure/cancer/master/public/screenshots/Anti-PD-1_Lung.jpg',
|
||||
SeriesDescription: 'Anti-PD-1_Lung',
|
||||
SeriesNumber: '2',
|
||||
numImageFrames: 512,
|
||||
displaySetInstanceUID: '10001-1',
|
||||
stackPercentComplete: 30,
|
||||
},
|
||||
{
|
||||
imageSrc:
|
||||
'https://raw.githubusercontent.com/crowds-cure/cancer/master/public/screenshots/Anti-PD-1_MELANOMA.jpg',
|
||||
SeriesDescription: 'Anti-PD-1_MELANOMA',
|
||||
SeriesNumber: '2',
|
||||
InstanceNumber: '1',
|
||||
numImageFrames: 256,
|
||||
displaySetInstanceUID: '10001-2',
|
||||
stackPercentComplete: 70,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
StudyInstanceUID: '10002',
|
||||
StudyDescription: 'CPTAC',
|
||||
modalities: 'CT',
|
||||
StudyDate: '16-aug-2017',
|
||||
active: true,
|
||||
thumbnails: [
|
||||
{
|
||||
imageSrc:
|
||||
'https://raw.githubusercontent.com/crowds-cure/cancer/master/public/screenshots/CPTAC-GBM.jpg',
|
||||
SeriesDescription: 'CPTAC-GBM',
|
||||
active: true,
|
||||
SeriesNumber: '2',
|
||||
numImageFrames: 512,
|
||||
displaySetInstanceUID: '10002-1',
|
||||
stackPercentComplete: 100,
|
||||
},
|
||||
{
|
||||
imageSrc:
|
||||
'https://raw.githubusercontent.com/crowds-cure/cancer/master/public/screenshots/CPTAC-CM.jpg',
|
||||
SeriesDescription: 'CPTAC-CM',
|
||||
SeriesNumber: '2',
|
||||
InstanceNumber: '1',
|
||||
displaySetInstanceUID: '10002-2',
|
||||
numImageFrames: 256,
|
||||
},
|
||||
{
|
||||
imageSrc:
|
||||
'https://raw.githubusercontent.com/crowds-cure/cancer/master/public/screenshots/CPTAC-HNSCC.jpg',
|
||||
SeriesDescription: 'CPTAC-HNSCC',
|
||||
SeriesNumber: '2',
|
||||
InstanceNumber: '1',
|
||||
displaySetInstanceUID: '10002-3',
|
||||
numImageFrames: 256,
|
||||
},
|
||||
{
|
||||
imageSrc:
|
||||
'https://raw.githubusercontent.com/crowds-cure/cancer/master/public/screenshots/CPTAC-LSCC.jpg',
|
||||
SeriesDescription: 'CPTAC-LSCC',
|
||||
SeriesNumber: '2',
|
||||
InstanceNumber: '1',
|
||||
displaySetInstanceUID: '10002-4',
|
||||
numImageFrames: 256,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
@ -1,84 +0,0 @@
|
||||
.RoundedButtonGroup {
|
||||
--height: 25px;
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.roundedButtonWrapper {
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
float: left;
|
||||
margin-left: -2px;
|
||||
text-decoration: none;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.roundedButtonWrapper.disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.RoundedButtonGroup .roundedButtonWrapper .roundedButton {
|
||||
align-items: center;
|
||||
background-color: var(--ui-gray-dark);
|
||||
border: 2px solid var(--ui-border-color-dark);
|
||||
color: var(--text-secondary-color);
|
||||
display: flex;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
justify-content: center;
|
||||
height: var(--height);
|
||||
line-height: var(--height);
|
||||
padding: 0 22px;
|
||||
position: relative;
|
||||
text-transform: uppercase;
|
||||
transition: var(--sidebar-transition);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.roundedButtonWrapper .roundedButton svg .roundedButtonWrapper .roundedButton span {
|
||||
margin: 0 2px;
|
||||
}
|
||||
|
||||
.roundedButtonWrapper .roundedButton i {
|
||||
line-height: 15px;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.roundedButtonWrapper .bottomLabel {
|
||||
padding: 0 10px 0 10px;
|
||||
}
|
||||
|
||||
.bottomLabel {
|
||||
color: var(--text-secondary-color);
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
line-height: 12px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.roundedButtonWrapper:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.RoundedButtonGroup .roundedButtonWrapper:first-child .roundedButton {
|
||||
border-bottom-left-radius: var(--height);
|
||||
border-top-left-radius: var(--height);
|
||||
}
|
||||
|
||||
.RoundedButtonGroup .roundedButtonWrapper:last-child .roundedButton {
|
||||
border-bottom-right-radius: var(--height);
|
||||
border-top-right-radius: var(--height);
|
||||
}
|
||||
|
||||
.roundedButtonWrapper:hover .roundedButton {
|
||||
background-color: var(--box-background-color);
|
||||
color: var(--ui-gray-dark);
|
||||
}
|
||||
|
||||
.roundedButtonWrapper.active .roundedButton {
|
||||
background-color: var(--active-color);
|
||||
border-color: var(--ui-border-color-active);
|
||||
color: var(--ui-gray-dark);
|
||||
z-index: 2;
|
||||
}
|
||||
@ -1,196 +0,0 @@
|
||||
.sb-container {
|
||||
position: fixed;
|
||||
width: var(--snackbar-size);
|
||||
padding: 20px;
|
||||
z-index: var(--snackbar-zIndex);
|
||||
box-sizing: border-box;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.sb-topLeft {
|
||||
top: 0;
|
||||
bottom: auto;
|
||||
left: 0;
|
||||
right: auto;
|
||||
}
|
||||
|
||||
.sb-topCenter {
|
||||
left: 50%;
|
||||
top: 0;
|
||||
bottom: auto;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.sb-topRight {
|
||||
top: 0;
|
||||
bottom: auto;
|
||||
left: auto;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.sb-bottomLeft {
|
||||
top: auto;
|
||||
bottom: 0px;
|
||||
left: 0px;
|
||||
right: auto;
|
||||
}
|
||||
|
||||
.sb-bottomCenter {
|
||||
left: 50%;
|
||||
bottom: 0;
|
||||
top: auto;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.sb-bottomRight {
|
||||
top: auto;
|
||||
bottom: 0px;
|
||||
left: auto;
|
||||
right: 0px;
|
||||
margin: 10px 0 0;
|
||||
}
|
||||
|
||||
.sb-topLeft .sb-item,
|
||||
.sb-topCenter .sb-item,
|
||||
.sb-topRight .sb-item {
|
||||
margin: 10px 0 0;
|
||||
}
|
||||
|
||||
.sb-bottomLeft .sb-item,
|
||||
.sb-bottomCenter .sb-item,
|
||||
.sb-bottomRight .sb-item {
|
||||
margin: 0 0 10px;
|
||||
}
|
||||
|
||||
.sb-closeBtn {
|
||||
height: 20px;
|
||||
opacity: 1;
|
||||
overflow: hidden;
|
||||
padding: 2px;
|
||||
text-align: center;
|
||||
text-shadow: none;
|
||||
width: 20px;
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
right: 5px;
|
||||
top: 5px;
|
||||
transition: all 0.3s ease;
|
||||
background: rgba(255, 255, 255, 0.6);
|
||||
border-radius: 100%;
|
||||
}
|
||||
|
||||
.sb-closeBtn:hover {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.sb-closeIcon {
|
||||
display: block;
|
||||
font-size: 0;
|
||||
height: 100%;
|
||||
line-height: 0;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.sb-closeIcon:after,
|
||||
.sb-closeIcon:before {
|
||||
content: ' ';
|
||||
display: block;
|
||||
height: 2px;
|
||||
transition: all 0.3s ease;
|
||||
width: 12px;
|
||||
background-color: #222;
|
||||
opacity: 1;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.sb-closeIcon:before {
|
||||
left: 4px;
|
||||
top: 3px;
|
||||
|
||||
transform: rotate(45deg);
|
||||
transform-origin: 0px 50%;
|
||||
}
|
||||
|
||||
.sb-closeIcon:after {
|
||||
right: 3px;
|
||||
top: 5px;
|
||||
transform: rotate(-45deg);
|
||||
transform-origin: calc(100% - 3px) 50%;
|
||||
}
|
||||
|
||||
.sb-title {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.sb-message {
|
||||
font-size: 14px;
|
||||
word-break: normal;
|
||||
}
|
||||
|
||||
.sb-item {
|
||||
position: relative;
|
||||
transition: height 300ms ease;
|
||||
animation: fadein 1s;
|
||||
padding: 20px;
|
||||
color: white;
|
||||
overflow: hidden;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 6px 10px 0 rgba(0, 0, 0, 0.2), 0 1px 18px 0 rgba(0, 0, 0, 0.12),
|
||||
0 3px 5px -1px rgba(0, 0, 0, 0.14);
|
||||
}
|
||||
|
||||
.sb-item a {
|
||||
color: white;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.sb-container .sb-hidden {
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
margin-bottom: 0;
|
||||
height: 0;
|
||||
opacity: 0;
|
||||
transition: all 300ms ease;
|
||||
}
|
||||
|
||||
.sb-success {
|
||||
background-color: var(--snackbar-success);
|
||||
}
|
||||
|
||||
.sb-error {
|
||||
background-color: var(--snackbar-error);
|
||||
}
|
||||
|
||||
.sb-warning {
|
||||
background-color: var(--snackbar-warning);
|
||||
}
|
||||
|
||||
.sb-info {
|
||||
background-color: var(--snackbar-info);
|
||||
}
|
||||
|
||||
@keyframes fadein {
|
||||
from {
|
||||
opacity: 0;
|
||||
top: 30px;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Internet Explorer */
|
||||
@-ms-keyframes fadein {
|
||||
from {
|
||||
opacity: 0;
|
||||
top: 30px;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
@ -1,144 +0,0 @@
|
||||
/* global cornerstone */
|
||||
import './ImageThumbnail.styl';
|
||||
|
||||
import { utils } from '@ohif/core';
|
||||
import React, { useState, useEffect, createRef } from 'react';
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import ViewportErrorIndicator from '../../viewer/ViewportErrorIndicator';
|
||||
import ViewportLoadingIndicator from '../../viewer/ViewportLoadingIndicator';
|
||||
|
||||
// TODO: How should we have this component depend on Cornerstone?
|
||||
// - Passed in as a prop?
|
||||
// - Set as external dependency?
|
||||
// - Pass in the entire load and render function as a prop?
|
||||
//import cornerstone from 'cornerstone-core';
|
||||
function ImageThumbnail(props) {
|
||||
const {
|
||||
width,
|
||||
height,
|
||||
imageSrc,
|
||||
imageId,
|
||||
stackPercentComplete,
|
||||
error: propsError,
|
||||
} = props;
|
||||
|
||||
const [isLoading, setLoading] = useState(false);
|
||||
const [error, setError] = useState(false);
|
||||
const [image, setImage] = useState({});
|
||||
const canvasRef = createRef();
|
||||
|
||||
let loadingOrError;
|
||||
let cancelablePromise;
|
||||
|
||||
if (propsError || error) {
|
||||
loadingOrError = <ViewportErrorIndicator />;
|
||||
} else if (isLoading) {
|
||||
loadingOrError = <ViewportLoadingIndicator />;
|
||||
}
|
||||
|
||||
const showStackLoadingProgressBar = stackPercentComplete !== undefined;
|
||||
|
||||
const shouldRenderToCanvas = () => {
|
||||
return imageId && !imageSrc;
|
||||
};
|
||||
|
||||
const fetchImagePromise = () => {
|
||||
if (!cancelablePromise) {
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
cancelablePromise
|
||||
.then(response => {
|
||||
setImage(response);
|
||||
})
|
||||
.catch(error => {
|
||||
if (error.isCanceled) return;
|
||||
setLoading(false);
|
||||
setError(true);
|
||||
throw new Error(error);
|
||||
});
|
||||
};
|
||||
|
||||
const setImagePromise = () => {
|
||||
if (shouldRenderToCanvas()) {
|
||||
cancelablePromise = utils.makeCancelable(
|
||||
cornerstone.loadAndCacheImage(imageId)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const purgeCancelablePromise = () => {
|
||||
if (cancelablePromise) {
|
||||
cancelablePromise.cancel();
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
purgeCancelablePromise();
|
||||
};
|
||||
}, [purgeCancelablePromise]);
|
||||
|
||||
useEffect(() => {
|
||||
if (image.imageId) {
|
||||
cornerstone.renderToCanvas(canvasRef.current, image);
|
||||
setLoading(false);
|
||||
}
|
||||
}, [canvasRef, image, image.imageId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!image.imageId || image.imageId !== imageId) {
|
||||
purgeCancelablePromise();
|
||||
setImagePromise();
|
||||
fetchImagePromise();
|
||||
}
|
||||
}, [fetchImagePromise, image.imageId, imageId, purgeCancelablePromise, setImagePromise]);
|
||||
|
||||
return (
|
||||
<div className="ImageThumbnail">
|
||||
<div className="image-thumbnail-canvas">
|
||||
{shouldRenderToCanvas() ? (
|
||||
<canvas ref={canvasRef} width={width} height={height} />
|
||||
) : (
|
||||
<img
|
||||
className="static-image"
|
||||
src={imageSrc}
|
||||
//width={this.props.width}
|
||||
height={height}
|
||||
alt={''}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{loadingOrError}
|
||||
{showStackLoadingProgressBar && (
|
||||
<div className="image-thumbnail-progress-bar">
|
||||
<div
|
||||
className="image-thumbnail-progress-bar-inner"
|
||||
style={{ width: `${stackPercentComplete}%` }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{isLoading && <div className="image-thumbnail-loading-indicator"></div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ImageThumbnail.propTypes = {
|
||||
imageSrc: PropTypes.string,
|
||||
imageId: PropTypes.string,
|
||||
error: PropTypes.bool,
|
||||
width: PropTypes.number,
|
||||
height: PropTypes.number,
|
||||
stackPercentComplete: PropTypes.number.isRequired,
|
||||
};
|
||||
|
||||
ImageThumbnail.defaultProps = {
|
||||
error: false,
|
||||
stackPercentComplete: 0,
|
||||
width: 217,
|
||||
height: 123,
|
||||
};
|
||||
|
||||
export default ImageThumbnail;
|
||||
@ -1,103 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Thumbnail } from './Thumbnail.js';
|
||||
import './StudyBrowser.styl';
|
||||
|
||||
function StudyBrowser(props) {
|
||||
const {
|
||||
studies,
|
||||
onThumbnailClick,
|
||||
onThumbnailDoubleClick,
|
||||
supportsDrag,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<div className="study-browser">
|
||||
<div className="scrollable-study-thumbnails">
|
||||
{studies
|
||||
.map((study, studyIndex) => {
|
||||
const { StudyInstanceUID } = study;
|
||||
return study.thumbnails.map((thumb, thumbIndex) => {
|
||||
// TODO: Thumb has more props than we care about?
|
||||
const {
|
||||
altImageText,
|
||||
displaySetInstanceUID,
|
||||
imageId,
|
||||
InstanceNumber,
|
||||
numImageFrames,
|
||||
SeriesDescription,
|
||||
SeriesNumber,
|
||||
stackPercentComplete,
|
||||
} = thumb;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={thumb.displaySetInstanceUID}
|
||||
className="thumbnail-container"
|
||||
data-cy="thumbnail-list"
|
||||
>
|
||||
<Thumbnail
|
||||
supportsDrag={supportsDrag}
|
||||
key={`${studyIndex}_${thumbIndex}`}
|
||||
id={`${studyIndex}_${thumbIndex}`} // Unused?
|
||||
// Study
|
||||
StudyInstanceUID={StudyInstanceUID} // used by drop
|
||||
// Thumb
|
||||
altImageText={altImageText}
|
||||
imageId={imageId}
|
||||
InstanceNumber={InstanceNumber}
|
||||
displaySetInstanceUID={displaySetInstanceUID} // used by drop
|
||||
numImageFrames={numImageFrames}
|
||||
SeriesDescription={SeriesDescription}
|
||||
SeriesNumber={SeriesNumber}
|
||||
stackPercentComplete={stackPercentComplete}
|
||||
// Events
|
||||
onClick={onThumbnailClick.bind(
|
||||
undefined,
|
||||
displaySetInstanceUID
|
||||
)}
|
||||
onDoubleClick={onThumbnailDoubleClick}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
})
|
||||
.flat()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const noop = () => {};
|
||||
|
||||
StudyBrowser.propTypes = {
|
||||
studies: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
StudyInstanceUID: PropTypes.string.isRequired,
|
||||
thumbnails: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
altImageText: PropTypes.string,
|
||||
displaySetInstanceUID: PropTypes.string.isRequired,
|
||||
imageId: PropTypes.string,
|
||||
InstanceNumber: PropTypes.number,
|
||||
numImageFrames: PropTypes.number,
|
||||
SeriesDescription: PropTypes.string,
|
||||
SeriesNumber: PropTypes.number,
|
||||
stackPercentComplete: PropTypes.number,
|
||||
})
|
||||
),
|
||||
})
|
||||
).isRequired,
|
||||
supportsDrag: PropTypes.bool,
|
||||
onThumbnailClick: PropTypes.func,
|
||||
onThumbnailDoubleClick: PropTypes.func,
|
||||
};
|
||||
|
||||
StudyBrowser.defaultProps = {
|
||||
studies: [],
|
||||
supportsDrag: true,
|
||||
onThumbnailClick: noop,
|
||||
onThumbnailDoubleClick: noop,
|
||||
};
|
||||
|
||||
export { StudyBrowser };
|
||||
@ -1,152 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useDrag } from 'react-dnd';
|
||||
import ImageThumbnail from './ImageThumbnail';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import './Thumbnail.styl';
|
||||
|
||||
function ThumbnailFooter({
|
||||
SeriesDescription,
|
||||
SeriesNumber,
|
||||
InstanceNumber,
|
||||
numImageFrames,
|
||||
}) {
|
||||
const infoOnly = !SeriesDescription;
|
||||
|
||||
const getInfo = (value, icon, className = '') => {
|
||||
return (
|
||||
<div className={classNames('item item-series', className)}>
|
||||
<div className="icon">{icon}</div>
|
||||
<div className="value">{value}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
const getSeriesInformation = (
|
||||
SeriesNumber,
|
||||
InstanceNumber,
|
||||
numImageFrames
|
||||
) => {
|
||||
if (!SeriesNumber && !InstanceNumber && !numImageFrames) {
|
||||
return;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="series-information">
|
||||
{getInfo(SeriesNumber, 'S:')}
|
||||
{getInfo(InstanceNumber, 'I:')}
|
||||
{getInfo(numImageFrames, '', 'image-frames')}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={classNames('series-details', { 'info-only': infoOnly })}>
|
||||
<div className="series-description">{SeriesDescription}</div>
|
||||
{getSeriesInformation(SeriesNumber, InstanceNumber, numImageFrames)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Thumbnail(props) {
|
||||
const {
|
||||
active,
|
||||
altImageText,
|
||||
error,
|
||||
displaySetInstanceUID,
|
||||
imageId,
|
||||
imageSrc,
|
||||
InstanceNumber,
|
||||
numImageFrames,
|
||||
SeriesDescription,
|
||||
SeriesNumber,
|
||||
stackPercentComplete,
|
||||
StudyInstanceUID,
|
||||
onClick,
|
||||
onDoubleClick,
|
||||
onMouseDown,
|
||||
supportsDrag,
|
||||
} = props;
|
||||
|
||||
const [collectedProps, drag, dragPreview] = useDrag({
|
||||
// `droppedItem` in `dropTarget`
|
||||
// The only data it will have access to
|
||||
item: {
|
||||
StudyInstanceUID,
|
||||
displaySetInstanceUID,
|
||||
type: 'thumbnail', // Has to match `dropTarget`'s type
|
||||
},
|
||||
canDrag: function(monitor) {
|
||||
return supportsDrag;
|
||||
},
|
||||
});
|
||||
|
||||
const hasImage = imageSrc || imageId;
|
||||
const hasAltText = altImageText !== undefined;
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={drag}
|
||||
className={classNames('thumbnail', { active: active })}
|
||||
onClick={onClick}
|
||||
onDoubleClick={onDoubleClick}
|
||||
onMouseDown={onMouseDown}
|
||||
>
|
||||
{/* SHOW IMAGE */}
|
||||
{hasImage && (
|
||||
<ImageThumbnail
|
||||
imageSrc={imageSrc}
|
||||
imageId={imageId}
|
||||
error={error}
|
||||
stackPercentComplete={stackPercentComplete}
|
||||
/>
|
||||
)}
|
||||
{/* SHOW TEXT ALTERNATIVE */}
|
||||
{!hasImage && hasAltText && (
|
||||
<div className={'alt-image-text p-x-1'}>
|
||||
<h1>{altImageText}</h1>
|
||||
</div>
|
||||
)}
|
||||
{ThumbnailFooter(props)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const noop = () => {};
|
||||
|
||||
Thumbnail.propTypes = {
|
||||
supportsDrag: PropTypes.bool,
|
||||
id: PropTypes.string.isRequired,
|
||||
displaySetInstanceUID: PropTypes.string.isRequired,
|
||||
StudyInstanceUID: PropTypes.string.isRequired,
|
||||
imageSrc: PropTypes.string,
|
||||
imageId: PropTypes.string,
|
||||
error: PropTypes.bool,
|
||||
active: PropTypes.bool,
|
||||
stackPercentComplete: PropTypes.number,
|
||||
/**
|
||||
altImageText will be used when no imageId or imageSrc is provided.
|
||||
It will be displayed inside the <div>. This is useful when it is difficult
|
||||
to make a preview for a type of DICOM series (e.g. DICOM-SR)
|
||||
*/
|
||||
altImageText: PropTypes.string,
|
||||
SeriesDescription: PropTypes.string,
|
||||
SeriesNumber: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
InstanceNumber: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
numImageFrames: PropTypes.number,
|
||||
onDoubleClick: PropTypes.func,
|
||||
onClick: PropTypes.func,
|
||||
onMouseDown: PropTypes.func,
|
||||
};
|
||||
|
||||
Thumbnail.defaultProps = {
|
||||
supportsDrag: false,
|
||||
active: false,
|
||||
error: false,
|
||||
stackPercentComplete: 0,
|
||||
onDoubleClick: noop,
|
||||
onClick: noop,
|
||||
onMouseDown: noop,
|
||||
};
|
||||
|
||||
export { Thumbnail };
|
||||
@ -1,84 +0,0 @@
|
||||
import React, { Component } from 'react';
|
||||
import { DropTarget } from 'react-dnd';
|
||||
import PropTypes from 'prop-types';
|
||||
import './ExampleDropTarget.css';
|
||||
|
||||
// Drag sources and drop targets only interact
|
||||
// if they have the same string type.
|
||||
const Types = {
|
||||
THUMBNAIL: 'thumbnail',
|
||||
};
|
||||
|
||||
const divTarget = {
|
||||
drop(props, monitor, component) {
|
||||
// Note: For this example we use setState, but in
|
||||
// OHIF we will update the redux store instead
|
||||
const item = monitor.getItem();
|
||||
|
||||
component.setState({
|
||||
item: {
|
||||
id: item.id,
|
||||
SeriesDescription: item.SeriesDescription,
|
||||
},
|
||||
});
|
||||
return { id: 'ExampleDropTarget' };
|
||||
},
|
||||
};
|
||||
|
||||
// TODO: Find out why we can't move this into the Example app instead.
|
||||
// It looks like the context isn't properly shared.
|
||||
class CustomDropTarget extends Component {
|
||||
static className = 'ExampleDropTarget';
|
||||
|
||||
state = {
|
||||
item: null,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
isOver: false,
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
connectDropTarget: PropTypes.func.isRequired,
|
||||
canDrop: PropTypes.bool.isRequired,
|
||||
isOver: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
render() {
|
||||
const { canDrop, isOver, connectDropTarget } = this.props;
|
||||
const isActive = canDrop && isOver;
|
||||
|
||||
let className = ExampleDropTarget.className;
|
||||
|
||||
if (isActive) {
|
||||
className += ' hovered';
|
||||
} else if (canDrop) {
|
||||
className += ' can-drop';
|
||||
}
|
||||
|
||||
return connectDropTarget(
|
||||
<div className={className}>
|
||||
<h4>
|
||||
{isActive
|
||||
? 'Release to drop'
|
||||
: 'Drag / Drop something from the Study Browser here'}
|
||||
</h4>
|
||||
<p className="study-drop-results">
|
||||
{this.state.item && JSON.stringify(this.state.item)}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const collect = (connect, monitor) => ({
|
||||
connectDropTarget: connect.dropTarget(),
|
||||
isOver: monitor.isOver(),
|
||||
canDrop: monitor.canDrop(),
|
||||
});
|
||||
|
||||
const ExampleDropTarget = DropTarget(Types.THUMBNAIL, divTarget, collect)(
|
||||
CustomDropTarget
|
||||
);
|
||||
|
||||
export { ExampleDropTarget };
|
||||
@ -1,75 +0,0 @@
|
||||
export const studies = [
|
||||
{
|
||||
thumbnails: [
|
||||
{
|
||||
imageSrc:
|
||||
'https://raw.githubusercontent.com/crowds-cure/cancer/master/public/screenshots/Anti-PD-1_Lung.jpg',
|
||||
SeriesDescription: 'Anti-PD-1_Lung',
|
||||
active: true,
|
||||
SeriesNumber: '2',
|
||||
numImageFrames: 512,
|
||||
stackPercentComplete: 30,
|
||||
},
|
||||
{
|
||||
imageSrc:
|
||||
'https://raw.githubusercontent.com/crowds-cure/cancer/master/public/screenshots/Anti-PD-1_MELANOMA.jpg',
|
||||
SeriesDescription: 'Anti-PD-1_MELANOMA',
|
||||
SeriesNumber: '2',
|
||||
InstanceNumber: '1',
|
||||
numImageFrames: 256,
|
||||
stackPercentComplete: 70,
|
||||
},
|
||||
{
|
||||
altImageText: 'SR',
|
||||
SeriesDescription: 'Imaging Measurement Report',
|
||||
SeriesNumber: '3',
|
||||
stackPercentComplete: 100,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
thumbnails: [
|
||||
{
|
||||
imageSrc:
|
||||
'https://raw.githubusercontent.com/crowds-cure/cancer/master/public/screenshots/CPTAC-GBM.jpg',
|
||||
SeriesDescription: 'CPTAC-GBM',
|
||||
active: true,
|
||||
SeriesNumber: '2',
|
||||
numImageFrames: 512,
|
||||
stackPercentComplete: 100,
|
||||
},
|
||||
{
|
||||
imageSrc:
|
||||
'https://raw.githubusercontent.com/crowds-cure/cancer/master/public/screenshots/CPTAC-CM.jpg',
|
||||
SeriesDescription: 'CPTAC-CM',
|
||||
SeriesNumber: '2',
|
||||
InstanceNumber: '1',
|
||||
numImageFrames: 256,
|
||||
},
|
||||
{
|
||||
imageSrc:
|
||||
'https://raw.githubusercontent.com/crowds-cure/cancer/master/public/screenshots/CPTAC-HNSCC.jpg',
|
||||
SeriesDescription: 'CPTAC-HNSCC',
|
||||
SeriesNumber: '2',
|
||||
InstanceNumber: '1',
|
||||
numImageFrames: 256,
|
||||
},
|
||||
{
|
||||
imageSrc:
|
||||
'https://raw.githubusercontent.com/crowds-cure/cancer/master/public/screenshots/CPTAC-LSCC.jpg',
|
||||
SeriesDescription: 'CPTAC-LSCC',
|
||||
SeriesNumber: '2',
|
||||
InstanceNumber: '1',
|
||||
numImageFrames: 256,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export function onThumbnailClick() {
|
||||
console.warn('onThumbnailClick');
|
||||
}
|
||||
|
||||
export function onThumbnailDoubleClick() {
|
||||
console.warn('onThumbnailDoubleClick');
|
||||
}
|
||||
@ -1,409 +0,0 @@
|
||||
import './StudyList.styl';
|
||||
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import TableSearchFilter from './TableSearchFilter.js';
|
||||
import PropTypes from 'prop-types';
|
||||
import { StudyListLoadingText } from './StudyListLoadingText.js';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const getContentFromUseMediaValue = (
|
||||
displaySize,
|
||||
contentArrayMap,
|
||||
defaultContent
|
||||
) => {
|
||||
const content =
|
||||
displaySize in contentArrayMap
|
||||
? contentArrayMap[displaySize]
|
||||
: defaultContent;
|
||||
|
||||
return content;
|
||||
};
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @param {*} props
|
||||
* @returns
|
||||
*/
|
||||
function StudyList(props) {
|
||||
const {
|
||||
isLoading,
|
||||
hasError,
|
||||
studies,
|
||||
sort,
|
||||
onSort: handleSort,
|
||||
filterValues,
|
||||
onFilterChange: handleFilterChange,
|
||||
onSelectItem: handleSelectItem,
|
||||
studyListDateFilterNumDays,
|
||||
displaySize,
|
||||
} = props;
|
||||
const { t, ready: translationsAreReady } = useTranslation('StudyList');
|
||||
|
||||
const largeTableMeta = [
|
||||
{
|
||||
displayText: t('PatientName'),
|
||||
fieldName: 'PatientName',
|
||||
inputType: 'text',
|
||||
size: 330,
|
||||
},
|
||||
{
|
||||
displayText: t('MRN'),
|
||||
fieldName: 'PatientID',
|
||||
inputType: 'text',
|
||||
size: 378,
|
||||
},
|
||||
{
|
||||
displayText: t('AccessionNumber'),
|
||||
fieldName: 'AccessionNumber',
|
||||
inputType: 'text',
|
||||
size: 180,
|
||||
},
|
||||
{
|
||||
displayText: t('StudyDate'),
|
||||
fieldName: 'StudyDate',
|
||||
inputType: 'date-range',
|
||||
size: 300,
|
||||
},
|
||||
{
|
||||
displayText: t('Modality'),
|
||||
fieldName: 'modalities',
|
||||
inputType: 'text',
|
||||
size: 114,
|
||||
},
|
||||
{
|
||||
displayText: t('StudyDescription'),
|
||||
fieldName: 'StudyDescription',
|
||||
inputType: 'text',
|
||||
size: 335,
|
||||
},
|
||||
];
|
||||
|
||||
const mediumTableMeta = [
|
||||
{
|
||||
displayText: `${t('PatientName')} / ${t('MRN')}`,
|
||||
fieldName: 'patientNameOrId',
|
||||
inputType: 'text',
|
||||
size: 250,
|
||||
},
|
||||
{
|
||||
displayText: t('Description'),
|
||||
fieldName: 'accessionOrModalityOrDescription',
|
||||
inputType: 'text',
|
||||
size: 350,
|
||||
},
|
||||
{
|
||||
displayText: t('StudyDate'),
|
||||
fieldName: 'StudyDate',
|
||||
inputType: 'date-range',
|
||||
size: 300,
|
||||
},
|
||||
];
|
||||
|
||||
const smallTableMeta = [
|
||||
{
|
||||
displayText: t('Search'),
|
||||
fieldName: 'allFields',
|
||||
inputType: 'text',
|
||||
size: 100,
|
||||
},
|
||||
];
|
||||
|
||||
const tableMeta = getContentFromUseMediaValue(
|
||||
displaySize,
|
||||
{ large: largeTableMeta, medium: mediumTableMeta, small: smallTableMeta },
|
||||
smallTableMeta
|
||||
);
|
||||
|
||||
const totalSize = tableMeta
|
||||
.map(field => field.size)
|
||||
.reduce((prev, next) => prev + next);
|
||||
|
||||
return translationsAreReady ? (
|
||||
<table className="table table--striped table--hoverable">
|
||||
<colgroup>
|
||||
{tableMeta.map((field, i) => {
|
||||
const size = field.size;
|
||||
const percentWidth = (size / totalSize) * 100.0;
|
||||
|
||||
return <col key={i} style={{ width: `${percentWidth}%` }} />;
|
||||
})}
|
||||
</colgroup>
|
||||
<thead className="table-head">
|
||||
<tr className="filters">
|
||||
<TableSearchFilter
|
||||
meta={tableMeta}
|
||||
values={filterValues}
|
||||
onSort={handleSort}
|
||||
onValueChange={handleFilterChange}
|
||||
sortFieldName={sort.fieldName}
|
||||
sortDirection={sort.direction}
|
||||
studyListDateFilterNumDays={studyListDateFilterNumDays}
|
||||
/>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="table-body" data-cy="study-list-results">
|
||||
{/* I'm not in love with this approach, but it's the quickest way for now
|
||||
*
|
||||
* - Display different content based on loading, empty, results state
|
||||
*
|
||||
* This is not ideal because it create a jump in focus. For loading especially,
|
||||
* We should keep our current results visible while we load the new ones.
|
||||
*/}
|
||||
{/* LOADING */}
|
||||
{isLoading && (
|
||||
<tr className="no-hover">
|
||||
<td colSpan={tableMeta.length}>
|
||||
<StudyListLoadingText />
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
{!isLoading && hasError && (
|
||||
<tr className="no-hover">
|
||||
<td colSpan={tableMeta.length}>
|
||||
<div className="notFound">
|
||||
{t('There was an error fetching studies')}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
{/* EMPTY */}
|
||||
{!isLoading && !studies.length && (
|
||||
<tr className="no-hover">
|
||||
<td colSpan={tableMeta.length}>
|
||||
<div className="notFound">{t('No matching results')}</div>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
{!isLoading &&
|
||||
studies.map((study, index) => (
|
||||
<TableRow
|
||||
key={`${study.StudyInstanceUID}-${index}`}
|
||||
onClick={StudyInstanceUID => handleSelectItem(StudyInstanceUID)}
|
||||
AccessionNumber={study.AccessionNumber || ''}
|
||||
modalities={study.modalities}
|
||||
PatientID={study.PatientID || ''}
|
||||
PatientName={study.PatientName || ''}
|
||||
StudyDate={study.StudyDate}
|
||||
StudyDescription={study.StudyDescription || ''}
|
||||
StudyInstanceUID={study.StudyInstanceUID}
|
||||
displaySize={displaySize}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
) : null;
|
||||
}
|
||||
|
||||
StudyList.propTypes = {
|
||||
isLoading: PropTypes.bool.isRequired,
|
||||
hasError: PropTypes.bool.isRequired,
|
||||
studies: PropTypes.array.isRequired,
|
||||
onSelectItem: PropTypes.func.isRequired,
|
||||
// ~~ SORT
|
||||
sort: PropTypes.shape({
|
||||
fieldName: PropTypes.string,
|
||||
direction: PropTypes.oneOf(['desc', 'asc', null]),
|
||||
}).isRequired,
|
||||
onSort: PropTypes.func.isRequired,
|
||||
// ~~ FILTERS
|
||||
filterValues: PropTypes.shape({
|
||||
PatientName: PropTypes.string.isRequired,
|
||||
PatientID: PropTypes.string.isRequired,
|
||||
AccessionNumber: PropTypes.string.isRequired,
|
||||
StudyDate: PropTypes.string.isRequired,
|
||||
modalities: PropTypes.string.isRequired,
|
||||
StudyDescription: PropTypes.string.isRequired,
|
||||
patientNameOrId: PropTypes.string.isRequired,
|
||||
accessionOrModalityOrDescription: PropTypes.string.isRequired,
|
||||
allFields: PropTypes.string.isRequired,
|
||||
studyDateTo: PropTypes.any,
|
||||
studyDateFrom: PropTypes.any,
|
||||
}).isRequired,
|
||||
onFilterChange: PropTypes.func.isRequired,
|
||||
studyListDateFilterNumDays: PropTypes.number,
|
||||
displaySize: PropTypes.string,
|
||||
};
|
||||
|
||||
StudyList.defaultProps = {};
|
||||
|
||||
function TableRow(props) {
|
||||
const {
|
||||
AccessionNumber,
|
||||
isHighlighted,
|
||||
modalities,
|
||||
PatientID,
|
||||
PatientName,
|
||||
StudyDate,
|
||||
StudyDescription,
|
||||
StudyInstanceUID,
|
||||
onClick: handleClick,
|
||||
displaySize,
|
||||
} = props;
|
||||
|
||||
const { t } = useTranslation('StudyList');
|
||||
|
||||
const largeRowTemplate = (
|
||||
<tr
|
||||
onClick={() => handleClick(StudyInstanceUID)}
|
||||
className={classNames({ active: isHighlighted })}
|
||||
>
|
||||
<td className={classNames({ 'empty-value': !PatientName })}>
|
||||
{PatientName || `(${t('Empty')})`}
|
||||
</td>
|
||||
<td>{PatientID}</td>
|
||||
<td>{AccessionNumber}</td>
|
||||
<td>{StudyDate}</td>
|
||||
<td className={classNames({ 'empty-value': !modalities })}>
|
||||
{modalities || `(${t('Empty')})`}
|
||||
</td>
|
||||
<td>{StudyDescription}</td>
|
||||
</tr>
|
||||
);
|
||||
|
||||
const mediumRowTemplate = (
|
||||
<tr
|
||||
onClick={() => handleClick(StudyInstanceUID)}
|
||||
className={classNames({ active: isHighlighted })}
|
||||
>
|
||||
<td className={classNames({ 'empty-value': !PatientName })}>
|
||||
{PatientName || `(${t('Empty')})`}
|
||||
<div style={{ color: '#60656f' }}>{PatientID}</div>
|
||||
</td>
|
||||
<td>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
{/* DESCRIPTION */}
|
||||
<div
|
||||
className="hide-xs"
|
||||
style={{
|
||||
whiteSpace: 'pre-wrap',
|
||||
flexGrow: 1,
|
||||
}}
|
||||
>
|
||||
{StudyDescription}
|
||||
</div>
|
||||
|
||||
{/* MODALITY & ACCESSION */}
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
maxWidth: '80px',
|
||||
width: '80px',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={classNames({
|
||||
modalities: modalities,
|
||||
'empty-value': !modalities,
|
||||
})}
|
||||
aria-label={modalities}
|
||||
title={modalities}
|
||||
>
|
||||
{modalities || `(${t('Empty')})`}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
aria-label={AccessionNumber}
|
||||
title={AccessionNumber}
|
||||
>
|
||||
{AccessionNumber}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
{/* DATE */}
|
||||
<td style={{ textAlign: 'center' }}>{StudyDate}</td>
|
||||
</tr>
|
||||
);
|
||||
|
||||
const smallRowTemplate = (
|
||||
<tr
|
||||
onClick={() => handleClick(StudyInstanceUID)}
|
||||
className={classNames({ active: isHighlighted })}
|
||||
>
|
||||
<td style={{ position: 'relative', overflow: 'hidden' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
{/* NAME AND ID */}
|
||||
<div
|
||||
className={classNames({ 'empty-value': !PatientName })}
|
||||
style={{ width: '150px', minWidth: '150px' }}
|
||||
>
|
||||
<div style={{ fontWeight: 500, paddingTop: '3px' }}>
|
||||
{PatientName || `(${t('Empty')})`}
|
||||
</div>
|
||||
<div style={{ color: '#60656f' }}>{PatientID}</div>
|
||||
</div>
|
||||
|
||||
{/* DESCRIPTION */}
|
||||
<div
|
||||
className="hide-xs"
|
||||
style={{
|
||||
whiteSpace: 'pre-wrap',
|
||||
flexGrow: 1,
|
||||
paddingLeft: '35px',
|
||||
}}
|
||||
>
|
||||
{StudyDescription}
|
||||
</div>
|
||||
|
||||
{/* MODALITY & DATE */}
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
maxWidth: '80px',
|
||||
width: '80px',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={classNames({
|
||||
modalities: modalities,
|
||||
'empty-value': !modalities,
|
||||
})}
|
||||
aria-label={modalities}
|
||||
title={modalities}
|
||||
>
|
||||
{modalities || `(${t('Empty')})`}
|
||||
</div>
|
||||
<div>{StudyDate}</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
|
||||
const rowTemplate = getContentFromUseMediaValue(
|
||||
displaySize,
|
||||
{
|
||||
large: largeRowTemplate,
|
||||
medium: mediumRowTemplate,
|
||||
small: smallRowTemplate,
|
||||
},
|
||||
smallRowTemplate
|
||||
);
|
||||
|
||||
return rowTemplate;
|
||||
}
|
||||
|
||||
TableRow.propTypes = {
|
||||
AccessionNumber: PropTypes.string.isRequired,
|
||||
isHighlighted: PropTypes.bool,
|
||||
modalities: PropTypes.string,
|
||||
PatientID: PropTypes.string.isRequired,
|
||||
PatientName: PropTypes.string.isRequired,
|
||||
StudyDate: PropTypes.string.isRequired,
|
||||
StudyDescription: PropTypes.string.isRequired,
|
||||
StudyInstanceUID: PropTypes.string.isRequired,
|
||||
displaySize: PropTypes.string,
|
||||
};
|
||||
|
||||
TableRow.defaultProps = {
|
||||
isHighlighted: false,
|
||||
};
|
||||
|
||||
export { StudyList };
|
||||
@ -1,238 +0,0 @@
|
||||
@import './../../design/styles/common/form.styl'
|
||||
@import './../../design/styles/common/global.styl'
|
||||
|
||||
$study-list-padding = 8%;
|
||||
$study-list-toolbar-height = 75px;
|
||||
$body-cell-height = 40px;
|
||||
$body-cell-top-bottom-padding = 16px;
|
||||
|
||||
$tablePaddingMediumScreen = 5px
|
||||
$tablePaddingBigScreen = 3%
|
||||
$tablePaddingLargeScreen = 5%
|
||||
|
||||
// TODO: move this to a common mixin files
|
||||
placeholder-color(c)
|
||||
&::-webkit-input-placeholder
|
||||
color: c
|
||||
&:-moz-placeholder
|
||||
color: c
|
||||
&::-moz-placeholder
|
||||
color: c
|
||||
&:-ms-input-placeholder
|
||||
color: c
|
||||
|
||||
.study-list-header
|
||||
.addNewStudy
|
||||
margin: 0 10px
|
||||
|
||||
label
|
||||
font-weight: 400
|
||||
cursor: pointer
|
||||
display: inline-block;
|
||||
max-width: 100%;
|
||||
margin-bottom: 5px;
|
||||
|
||||
input
|
||||
width: 0.1px;
|
||||
height: 0.1px
|
||||
opacity: 0
|
||||
overflow: hidden
|
||||
position: absolute
|
||||
z-index: -1
|
||||
|
||||
color: var(--text-secondary-color)
|
||||
|
||||
&:hover
|
||||
color: var(--hover-color)
|
||||
|
||||
&:active
|
||||
color: var(--active-color)
|
||||
|
||||
.study-list-header
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
background-color: var(--ui-gray-darker)
|
||||
height: $study-list-toolbar-height
|
||||
margin-bottom: 2px
|
||||
padding: 0 $study-list-padding
|
||||
line-height: $study-list-toolbar-height
|
||||
|
||||
.header
|
||||
font-size: 22px
|
||||
font-weight: 300
|
||||
color: var(--table-text-secondary-color)
|
||||
line-height: $study-list-toolbar-height
|
||||
|
||||
.actions
|
||||
display: flex;
|
||||
|
||||
.study-count
|
||||
color: var(--large-numbers-color)
|
||||
font-size: 40px
|
||||
font-weight: 100
|
||||
line-height: $study-list-toolbar-height
|
||||
|
||||
/*
|
||||
* Dark gray background with blue border
|
||||
* Spans width of page to create a distinct area for table filters
|
||||
*/
|
||||
.table-head-background
|
||||
height: 121px
|
||||
position: absolute
|
||||
width: 100%
|
||||
|
||||
&:before, &:after
|
||||
content: ''
|
||||
display: block
|
||||
left: 0
|
||||
position: absolute
|
||||
width: 100%
|
||||
|
||||
&:before
|
||||
background-color: var(--ui-gray-darker)
|
||||
height: 100%
|
||||
top: 0
|
||||
z-index: 1
|
||||
|
||||
&:after
|
||||
background-color: var(--ui-border-color-active)
|
||||
bottom: -1px
|
||||
height: 1px
|
||||
z-index: 2
|
||||
|
||||
|
||||
.study-list-container
|
||||
width: 100%
|
||||
padding: 0 $study-list-padding
|
||||
position: absolute
|
||||
z-index: 2
|
||||
|
||||
table.table
|
||||
width: 100%;
|
||||
margin-bottom: 20px;
|
||||
border-spacing: 0;
|
||||
border-collapse: collapse;
|
||||
table-layout: fixed;
|
||||
color: var(--table-text-primary-color)
|
||||
font-weight: 300
|
||||
|
||||
> tbody tr
|
||||
padding: 5px
|
||||
background-color: black
|
||||
|
||||
> tbody td
|
||||
padding: $body-cell-top-bottom-padding 8px;
|
||||
height: $body-cell-height
|
||||
word-wrap: break-word;
|
||||
|
||||
/* Striped Variant */
|
||||
&.table--striped > tbody tr:nth-child(even)
|
||||
background-color: var(--ui-gray-darker)
|
||||
|
||||
/* Hover Variant */
|
||||
&.table--hoverable > tbody tr
|
||||
&:hover, &:active, &.active
|
||||
background-color: var(--table-hover-color)
|
||||
&.no-hover
|
||||
&:hover, &:active, &.active
|
||||
background-color: var(--ui-gray-darker)
|
||||
|
||||
.empty-value
|
||||
color: var(--ui-gray-light)
|
||||
|
||||
.study-list-container > table.table > tr
|
||||
height: 20px
|
||||
|
||||
.study-list-container > table.table > thead
|
||||
::-webkit-datetime-edit-year-field:not([aria-valuenow]),
|
||||
::-webkit-datetime-edit-month-field:not([aria-valuenow]),
|
||||
::-webkit-datetime-edit-day-field:not([aria-valuenow])
|
||||
color: transparent
|
||||
|
||||
.study-list-container > table.table > thead > tr > th
|
||||
padding: 0;
|
||||
border-bottom: 1px solid var(--ui-border-color-active);
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
border-top: 0;
|
||||
|
||||
.study-list-container > table.table > thead > tr > th.StudyDate
|
||||
min-width: 230px
|
||||
.study-list-container > table.table .modalities
|
||||
font-weight: 500;
|
||||
min-height: 20px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
|
||||
.study-list-container
|
||||
.filters
|
||||
label
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
min-width: 95px;
|
||||
margin: 0 auto;
|
||||
color: var(--table-text-primary-color);
|
||||
font-weight: 400;
|
||||
padding: 20px 8px;
|
||||
user-select: none;
|
||||
font-size: 15px;
|
||||
|
||||
&:hover
|
||||
color: var(--active-color);
|
||||
|
||||
&.active, &:active
|
||||
color: var(--active-color);
|
||||
i
|
||||
margin: 0 5px;
|
||||
|
||||
input
|
||||
height: 40px
|
||||
margin: 0 5px 20px 5px
|
||||
padding: 0 20px
|
||||
cursor: pointer
|
||||
border: none
|
||||
background-color: var(--input-background-color)
|
||||
color: var(--input-placeholder-color)
|
||||
font-size: 10pt
|
||||
font-weight: normal
|
||||
border-radius: 4px
|
||||
width: calc(100% - 10px); /* Just use padding? */
|
||||
transition(all 0.15s ease)
|
||||
placeholder-color(var(--input-placeholder-color))
|
||||
|
||||
&:active, &:hover
|
||||
background-color: var(--input-background-color)
|
||||
|
||||
.loading-text
|
||||
color: var(--table-text-secondary-color)
|
||||
text-align: center;
|
||||
font-size: 30px
|
||||
|
||||
.notFound
|
||||
color: var(--table-text-secondary-color)
|
||||
font-size: 30px
|
||||
font-weight: 200
|
||||
text-align: center
|
||||
|
||||
@media only screen and (max-width: 768px)
|
||||
.study-list-header
|
||||
padding: 0 16px;
|
||||
|
||||
.study-list-container
|
||||
padding: 0;
|
||||
|
||||
.study-list-container > table.table > thead > tr > th
|
||||
padding: 0 13px;
|
||||
|
||||
.study-list-container > table.table > tbody > tr > td
|
||||
padding: 8px;
|
||||
|
||||
.study-list-container .filters label
|
||||
padding: 8px;
|
||||
|
||||
@media only screen and (max-width: 500px)
|
||||
.hide-xs
|
||||
display: none;
|
||||
@ -1,106 +0,0 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import './PaginationArea.styl';
|
||||
import { withTranslation } from '../../contextProviders';
|
||||
|
||||
class TablePagination extends PureComponent {
|
||||
static defaultProps = {
|
||||
pageOptions: [5, 10, 25, 50, 100],
|
||||
rowsPerPage: 25,
|
||||
currentPage: 0,
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
/* Values to show in "Rows per page" select dropdown */
|
||||
pageOptions: PropTypes.array,
|
||||
rowsPerPage: PropTypes.number.isRequired,
|
||||
currentPage: PropTypes.number.isRequired,
|
||||
nextPageFunc: PropTypes.func,
|
||||
prevPageFunc: PropTypes.func,
|
||||
onRowsPerPageChange: PropTypes.func,
|
||||
recordCount: PropTypes.number.isRequired,
|
||||
};
|
||||
|
||||
nextPage = () => {
|
||||
this.props.nextPageFunc(this.props.currentPage);
|
||||
};
|
||||
|
||||
prevPage = () => {
|
||||
this.props.prevPageFunc(this.props.currentPage);
|
||||
};
|
||||
|
||||
onRowsPerPageChange = event => {
|
||||
this.props.onRowsPerPageChange(parseInt(event.target.value));
|
||||
};
|
||||
|
||||
renderPaginationButtons() {
|
||||
return (
|
||||
<div className="col-xs-8 col-sm-9 col-md-9">
|
||||
<div className="form-inline form-group page-buttons noselect">
|
||||
<React.Fragment>
|
||||
<ul className="pagination-control no-margins">
|
||||
<li className="page-item prev">
|
||||
<button
|
||||
onClick={this.prevPage}
|
||||
disabled={this.props.currentPage === 0}
|
||||
className="btn page-link"
|
||||
>
|
||||
{this.props.t('Previous')}
|
||||
</button>
|
||||
</li>
|
||||
<li className="page-item next">
|
||||
<button
|
||||
onClick={this.nextPage}
|
||||
disabled={
|
||||
this.props.recordCount === 0 ||
|
||||
this.props.rowsPerPage > this.props.recordCount
|
||||
}
|
||||
className="btn page-link"
|
||||
>
|
||||
{this.props.t('Next')}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</React.Fragment>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderRowsPerPageDropdown() {
|
||||
return (
|
||||
<div className="form-inline form-group rows-per-page">
|
||||
<span>{this.props.t('Show')}</span>
|
||||
<select
|
||||
onChange={this.onRowsPerPageChange}
|
||||
defaultValue={this.props.rowsPerPage}
|
||||
>
|
||||
{this.props.pageOptions.map(pageNumber => {
|
||||
return (
|
||||
<option key={pageNumber} value={pageNumber}>
|
||||
{pageNumber}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
<span>{this.props.t('RowsPerPage')}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="pagination-area">
|
||||
<div className="rows-dropdown">{this.renderRowsPerPageDropdown()}</div>
|
||||
<div className="pagination-buttons">
|
||||
<div className="form-inline form-group page-number pull-right">
|
||||
{this.renderPaginationButtons()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const connectedComponent = withTranslation('Common')(TablePagination);
|
||||
export { connectedComponent as TablePagination };
|
||||
@ -1,72 +0,0 @@
|
||||
import moment from 'moment';
|
||||
|
||||
export default function(searchData) {
|
||||
this.setState({ searchData });
|
||||
|
||||
const filter = (key, searchData, study) => {
|
||||
if (key === 'studyDateFrom' && searchData[key] && study['StudyDate']) {
|
||||
const StudyDate = moment(study['StudyDate'], 'YYYYMMDD');
|
||||
return StudyDate.isBetween(
|
||||
searchData['studyDateFrom'],
|
||||
searchData['studyDateTo'],
|
||||
'days',
|
||||
'[]'
|
||||
);
|
||||
} else if (searchData[key] && !study[key].includes(searchData[key])) {
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
const { field, order } = searchData.sortData;
|
||||
|
||||
// just a example of local filtering
|
||||
let filteredStudies = this.defaultStudies
|
||||
.filter(function(study) {
|
||||
const all = [
|
||||
'PatientName',
|
||||
'PatientID',
|
||||
'AccessionNumber',
|
||||
'modalities',
|
||||
'StudyDescription',
|
||||
'studyDateFrom',
|
||||
].every(key => {
|
||||
return filter(key, searchData, study);
|
||||
});
|
||||
|
||||
return all;
|
||||
})
|
||||
.sort(function(a, b) {
|
||||
if (order === 'desc') {
|
||||
if (a[field] < b[field]) {
|
||||
return -1;
|
||||
}
|
||||
if (a[field] > b[field]) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
} else {
|
||||
if (a[field] > b[field]) {
|
||||
return -1;
|
||||
}
|
||||
if (a[field] < b[field]) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
|
||||
// User can notice the loading icon
|
||||
return new Promise(resolve => {
|
||||
setTimeout(() => {
|
||||
const first = searchData.currentPage * searchData.rowsPerPage;
|
||||
let last =
|
||||
searchData.currentPage * searchData.rowsPerPage +
|
||||
searchData.rowsPerPage;
|
||||
last = last >= filteredStudies.length ? filteredStudies.length : last;
|
||||
this.setState({ studies: filteredStudies.slice(first, last) });
|
||||
resolve();
|
||||
}, 500);
|
||||
});
|
||||
}
|
||||
@ -1,87 +0,0 @@
|
||||
import moment from 'moment';
|
||||
|
||||
const studies = [
|
||||
{
|
||||
StudyInstanceUID: '11111.111111.111111.11111',
|
||||
PatientName: 'John Doe',
|
||||
PatientID: '1',
|
||||
AccessionNumber: '1234567',
|
||||
StudyDate: '19930201',
|
||||
modalities: 'MR',
|
||||
StudyDescription: 'BRAIN',
|
||||
},
|
||||
{
|
||||
StudyInstanceUID: '2222.222222.22222.22222',
|
||||
PatientName: 'José Silva',
|
||||
PatientID: '2',
|
||||
AccessionNumber: '7654321',
|
||||
StudyDate: moment().format('YYYYMMDD'),
|
||||
modalities: 'CT',
|
||||
StudyDescription: 'PET CT STANDARD',
|
||||
},
|
||||
{
|
||||
StudyInstanceUID: '3333.333333.33333.33333',
|
||||
PatientName: 'Antônio Jefferson',
|
||||
PatientID: '3',
|
||||
AccessionNumber: '732311',
|
||||
StudyDate: moment()
|
||||
.subtract(14, 'days')
|
||||
.format('YYYYMMDD'),
|
||||
modalities: 'US',
|
||||
StudyDescription: '0',
|
||||
},
|
||||
{
|
||||
StudyInstanceUID: '444444.44444.44444.4444',
|
||||
PatientName: 'Antonio da Silva',
|
||||
PatientID: '4',
|
||||
AccessionNumber: '732311',
|
||||
StudyDate: moment()
|
||||
.subtract(1, 'months')
|
||||
.format('YYYYMMDD'),
|
||||
modalities: 'US',
|
||||
StudyDescription: '0',
|
||||
},
|
||||
{
|
||||
StudyInstanceUID: '55555.55555.55555.55555',
|
||||
PatientName: 'Bezerra Souza',
|
||||
PatientID: '5',
|
||||
AccessionNumber: '5134543',
|
||||
StudyDate: moment()
|
||||
.subtract(6, 'days')
|
||||
.format('YYYYMMDD'),
|
||||
modalities: 'US',
|
||||
StudyDescription: '0',
|
||||
},
|
||||
{
|
||||
StudyInstanceUID: '66666.66666.66666.6666',
|
||||
PatientName: 'Geraldo Roger',
|
||||
PatientID: '6',
|
||||
AccessionNumber: '5315135',
|
||||
StudyDate: moment()
|
||||
.subtract(7, 'days')
|
||||
.format('YYYYMMDD'),
|
||||
modalities: 'US',
|
||||
StudyDescription: 'US',
|
||||
},
|
||||
{
|
||||
StudyInstanceUID: '77777.77777.77777.77777',
|
||||
PatientName: '',
|
||||
PatientID: '7',
|
||||
AccessionNumber: '5315136',
|
||||
StudyDate: moment()
|
||||
.subtract(5, 'days')
|
||||
.format('YYYYMMDD'),
|
||||
modalities: 'US',
|
||||
StudyDescription: 'US',
|
||||
},
|
||||
].sort(function(a, b) {
|
||||
if (a.PatientName < b.PatientName) {
|
||||
return -1;
|
||||
}
|
||||
if (a.PatientName > b.PatientName) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
export default studies;
|
||||
@ -1,53 +0,0 @@
|
||||
---
|
||||
name: Study List
|
||||
menu: Components
|
||||
route: /components/study-list
|
||||
---
|
||||
|
||||
import { Playground, Props } from 'docz'
|
||||
import { State } from 'react-powerplug'
|
||||
import { StudyList } from './../index.js'
|
||||
// Data
|
||||
import defaultStudies from './studies.js'
|
||||
import onSearch from './onSearch.js'
|
||||
import moment from 'moment'
|
||||
|
||||
# Study List
|
||||
|
||||
## Basic usage
|
||||
|
||||
<Playground>
|
||||
<State initial={{
|
||||
searchData: {},
|
||||
studies: defaultStudies.filter(study => {
|
||||
const StudyDate = moment(study['StudyDate'], 'YYYYMMDD');
|
||||
const startDate = moment().subtract(5, 'days');
|
||||
const endDate = moment();
|
||||
|
||||
return StudyDate.isBetween(startDate, endDate, 'days', '[]');
|
||||
}).slice(0, 5)
|
||||
}}>
|
||||
{({ state, setState }) => (
|
||||
<div className="row" style={{ backgroundColor: '#000', height: '600px' }}>
|
||||
<div className="col-xs-12" style={{ padding: 0 }}>
|
||||
<StudyList
|
||||
studies={state.studies}
|
||||
pageOptions={[1, 2, 3, 5, 10, 15, 20, 25, 50, 100]}
|
||||
studyListFunctionsEnabled={true}
|
||||
onImport={(e) => alert('Import study mock ' + e)}
|
||||
onSelectItem={(StudyInstanceUID) => { alert(StudyInstanceUID + ' has selected! Now you can open your study.'); }}
|
||||
rowsPerPage={5}
|
||||
defaultSort={{ field: 'PatientName', order: 'desc', }}
|
||||
studyListDateFilterNumDays={7}
|
||||
onSearch={onSearch.bind({ setState, defaultStudies })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</State>
|
||||
|
||||
</Playground>
|
||||
|
||||
## API
|
||||
|
||||
<Props of={StudyList} />
|
||||
@ -1,44 +0,0 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import './TableList.styl';
|
||||
|
||||
export class TableList extends Component {
|
||||
static propTypes = {
|
||||
customHeader: PropTypes.node,
|
||||
defaultItems: PropTypes.object,
|
||||
children: PropTypes.node.isRequired,
|
||||
headerTitle: PropTypes.string,
|
||||
headless: PropTypes.bool,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
headless: false,
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="tableList">
|
||||
{!this.props.headless && (
|
||||
<div className="tableListHeader" onClick={this.onHeaderClick}>
|
||||
{this.getHeader()}
|
||||
</div>
|
||||
)}
|
||||
<div className="tableListContent">{this.props.children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
getHeader = () => {
|
||||
if (this.props.customHeader) {
|
||||
return this.props.customHeader;
|
||||
} else {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div className="tableListHeaderTitle">{this.props.headerTitle}</div>
|
||||
<div className="numberOfItems">{this.props.children.length}</div>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
@ -1,53 +0,0 @@
|
||||
import './TableListItem.styl';
|
||||
|
||||
import { Component } from 'react';
|
||||
import { Icon } from './../../elements/Icon';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
|
||||
export class TableListItem extends Component {
|
||||
static propTypes = {
|
||||
children: PropTypes.node,
|
||||
itemClass: PropTypes.string,
|
||||
itemIndex: PropTypes.number,
|
||||
itemMeta: PropTypes.node,
|
||||
itemMetaClass: PropTypes.string,
|
||||
itemKey: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.number,
|
||||
]),
|
||||
onItemClick: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
itemMeta: null,
|
||||
itemMetaClass: ''
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div
|
||||
className={`tableListItem ${this.props.itemClass}`}
|
||||
onClick={this.onItemClick}
|
||||
>
|
||||
<div className={`itemIndex ${this.props.itemMetaClass}`}>
|
||||
{this.props.itemIndex}
|
||||
{this.props.itemMeta}
|
||||
<span className="warning-icon">
|
||||
<Icon name="exclamation-triangle" />
|
||||
</span>
|
||||
</div>
|
||||
<div className="itemContent">{this.props.children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
onItemClick = event => {
|
||||
if (this.props.onItemClick) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
this.props.onItemClick(event, this.props.itemKey);
|
||||
}
|
||||
};
|
||||
}
|
||||
@ -1,194 +0,0 @@
|
||||
import React from 'react';
|
||||
import adjust from './icons/adjust.svg';
|
||||
// Icons
|
||||
import angleDoubleDown from './icons/angle-double-down.svg';
|
||||
import angleDoubleUp from './icons/angle-double-up.svg';
|
||||
import angleLeft from './icons/angle-left.svg';
|
||||
import arrows from './icons/arrows.svg';
|
||||
import arrowsAltH from './icons/arrows-alt-h.svg';
|
||||
import arrowsAltV from './icons/arrows-alt-v.svg';
|
||||
import bars from './icons/bars.svg';
|
||||
import brain from './icons/brain.svg';
|
||||
import brush from './icons/brush.svg';
|
||||
import caretDown from './icons/caret-down.svg';
|
||||
import caretUp from './icons/caret-up.svg';
|
||||
import check from './icons/check.svg';
|
||||
import checkCircle from './icons/check-circle.svg';
|
||||
import checkCircleO from './icons/check-circle-o.svg';
|
||||
import chevronDown from './icons/chevron-down.svg';
|
||||
import circle from './icons/circle.svg';
|
||||
import circleNotch from './icons/circle-notch.svg';
|
||||
import circleO from './icons/circle-o.svg';
|
||||
import cog from './icons/cog.svg';
|
||||
import createComment from './icons/create-comment.svg';
|
||||
import createScreenCapture from './icons/create-screen-capture.svg';
|
||||
import crosshairs from './icons/crosshairs.svg';
|
||||
import cube from './icons/cube.svg';
|
||||
import d3Rotate from './icons/3d-rotate.svg';
|
||||
import database from './icons/database.svg';
|
||||
import dotCircle from './icons/dot-circle.svg';
|
||||
import edit from './icons/edit.svg';
|
||||
import ellipseCircle from './icons/ellipse-circle.svg';
|
||||
import ellipseH from './icons/ellipse-h.svg';
|
||||
import ellipseV from './icons/ellipse-v.svg';
|
||||
import exclamationCircle from './icons/exclamation-circle.svg';
|
||||
import exclamationTriangle from './icons/exclamation-triangle.svg';
|
||||
import fastBackward from './icons/fast-backward.svg';
|
||||
import fastForward from './icons/fast-forward.svg';
|
||||
import stop from './icons/stop.svg';
|
||||
import info from './icons/info.svg';
|
||||
import inlineEdit from './icons/inline-edit.svg';
|
||||
import level from './icons/level.svg';
|
||||
import link from './icons/link.svg';
|
||||
import linkCircles from './icons/link-circles.svg';
|
||||
import list from './icons/list.svg';
|
||||
import liver from './icons/liver.svg';
|
||||
import lock from './icons/lock.svg';
|
||||
import lockAlt from './icons/lock-alt.svg';
|
||||
import lung from './icons/lung.svg';
|
||||
import measureNonTarget from './icons/measure-non-target.svg';
|
||||
import measureTarget from './icons/measure-target.svg';
|
||||
import measureTargetCr from './icons/measure-target-cr.svg';
|
||||
import measureTargetNe from './icons/measure-target-ne.svg';
|
||||
import measureTargetUn from './icons/measure-target-un.svg';
|
||||
import measureTemp from './icons/measure-temp.svg';
|
||||
import objectGroup from './icons/object-group.svg';
|
||||
import ohifLogo from './icons/ohif-logo.svg';
|
||||
import ohifTextLogo from './icons/ohif-text-logo.svg';
|
||||
import oval from './icons/oval.svg';
|
||||
import palette from './icons/palette.svg';
|
||||
import play from './icons/play.svg';
|
||||
import plus from './icons/plus.svg';
|
||||
import powerOff from './icons/power-off.svg';
|
||||
import reset from './icons/reset.svg';
|
||||
import rotate from './icons/rotate.svg';
|
||||
import rotateRight from './icons/rotate-right.svg';
|
||||
import saveRegular from './icons/save-regular.svg';
|
||||
import scissors from './icons/scissors.svg';
|
||||
import search from './icons/search.svg';
|
||||
import searchPlus from './icons/search-plus.svg';
|
||||
import softTissue from './icons/soft-tissue.svg';
|
||||
import sort from './icons/sort.svg';
|
||||
import sortDown from './icons/sort-down.svg';
|
||||
import sortUp from './icons/sort-up.svg';
|
||||
import sphere from './icons/sphere.svg';
|
||||
import squareO from './icons/square-o.svg';
|
||||
import star from './icons/star.svg';
|
||||
import stepBackward from './icons/step-backward.svg';
|
||||
import stepForward from './icons/step-forward.svg';
|
||||
import sun from './icons/sun.svg';
|
||||
import th from './icons/th.svg';
|
||||
import thLarge from './icons/th-large.svg';
|
||||
import thList from './icons/th-list.svg';
|
||||
import times from './icons/times.svg';
|
||||
import trash from './icons/trash.svg';
|
||||
import unlink from './icons/unlink.svg';
|
||||
import user from './icons/user.svg';
|
||||
import youtube from './icons/youtube.svg';
|
||||
import eye from './icons/eye.svg';
|
||||
import eyeClosed from './icons/eye-closed.svg';
|
||||
|
||||
const ICONS = {
|
||||
eye,
|
||||
'eye-closed': eyeClosed,
|
||||
brush,
|
||||
scissors,
|
||||
user,
|
||||
sort,
|
||||
th,
|
||||
star,
|
||||
'sort-up': sortUp,
|
||||
sphere,
|
||||
'sort-down': sortDown,
|
||||
info,
|
||||
cube,
|
||||
crosshairs,
|
||||
'dot-circle': dotCircle,
|
||||
'angle-left': angleLeft,
|
||||
'3d-rotate': d3Rotate,
|
||||
plus,
|
||||
'chevron-down': chevronDown,
|
||||
'angle-double-down': angleDoubleDown,
|
||||
'angle-double-up': angleDoubleUp,
|
||||
'arrows-alt-h': arrowsAltH,
|
||||
'arrows-alt-v': arrowsAltV,
|
||||
bars,
|
||||
'caret-down': caretDown,
|
||||
'caret-up': caretUp,
|
||||
'check-circle-o': checkCircleO,
|
||||
check,
|
||||
circle,
|
||||
'circle-o': circleO,
|
||||
times,
|
||||
'create-comment': createComment,
|
||||
'create-screen-capture': createScreenCapture,
|
||||
edit,
|
||||
'fast-backward': fastBackward,
|
||||
'fast-forward': fastForward,
|
||||
'object-group': objectGroup,
|
||||
search,
|
||||
'power-off': powerOff,
|
||||
'inline-edit': inlineEdit,
|
||||
list,
|
||||
'ohif-logo': ohifLogo,
|
||||
'ohif-text-logo': ohifTextLogo,
|
||||
lock,
|
||||
play,
|
||||
database,
|
||||
cog,
|
||||
'circle-notch': circleNotch,
|
||||
'square-o': squareO,
|
||||
'check-circle': checkCircle,
|
||||
'lock-alt': lockAlt,
|
||||
'step-backward': stepBackward,
|
||||
'step-forward': stepForward,
|
||||
stop,
|
||||
'th-large': thLarge,
|
||||
'th-list': thList,
|
||||
sun,
|
||||
palette,
|
||||
youtube,
|
||||
oval,
|
||||
'ellipse-h': ellipseH,
|
||||
'ellipse-v': ellipseV,
|
||||
adjust,
|
||||
level,
|
||||
'link-circles': linkCircles,
|
||||
'search-plus': searchPlus,
|
||||
'measure-non-target': measureNonTarget,
|
||||
'measure-target': measureTarget,
|
||||
'measure-target-cr': measureTargetCr,
|
||||
'measure-target-un': measureTargetUn,
|
||||
'measure-target-ne': measureTargetNe,
|
||||
'measure-temp': measureTemp,
|
||||
'ellipse-circle': ellipseCircle,
|
||||
arrows,
|
||||
reset,
|
||||
rotate,
|
||||
'rotate-right': rotateRight,
|
||||
trash,
|
||||
unlink,
|
||||
'exclamation-circle': exclamationCircle,
|
||||
link,
|
||||
'exclamation-triangle': exclamationTriangle,
|
||||
brain,
|
||||
'soft-tissue': softTissue,
|
||||
lung,
|
||||
liver,
|
||||
save: saveRegular,
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the matching SVG Icon as a React Component.
|
||||
* Results in an inlined SVG Element. If there's no match,
|
||||
* return `null`
|
||||
*/
|
||||
export default function getIcon(key, props) {
|
||||
if (!key || !ICONS[key]) {
|
||||
return React.createElement('div', null, 'Missing Icon');
|
||||
}
|
||||
|
||||
return React.createElement(ICONS[key], props);
|
||||
}
|
||||
|
||||
export { ICONS };
|
||||
@ -1,11 +0,0 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 512 512"
|
||||
aria-labelledby="title"
|
||||
width="1em"
|
||||
height="1em"
|
||||
fill="currentColor"
|
||||
>
|
||||
<title id="title">Brush</title>
|
||||
<path d="M167.02 309.34c-40.12 2.58-76.53 17.86-97.19 72.3-2.35 6.21-8 9.98-14.59 9.98-11.11 0-45.46-27.67-55.25-34.35C0 439.62 37.93 512 128 512c75.86 0 128-43.77 128-120.19 0-3.11-.65-6.08-.97-9.13l-88.01-73.34zM457.89 0c-15.16 0-29.37 6.71-40.21 16.45C213.27 199.05 192 203.34 192 257.09c0 13.7 3.25 26.76 8.73 38.7l63.82 53.18c7.21 1.8 14.64 3.03 22.39 3.03 62.11 0 98.11-45.47 211.16-256.46 7.38-14.35 13.9-29.85 13.9-45.99C512 20.64 486 0 457.89 0z"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 646 B |
@ -1 +0,0 @@
|
||||
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="eye-slash" class="svg-inline--fa fa-eye-slash fa-w-20" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><path fill="currentColor" d="M320 400c-75.85 0-137.25-58.71-142.9-133.11L72.2 185.82c-13.79 17.3-26.48 35.59-36.72 55.59a32.35 32.35 0 0 0 0 29.19C89.71 376.41 197.07 448 320 448c26.91 0 52.87-4 77.89-10.46L346 397.39a144.13 144.13 0 0 1-26 2.61zm313.82 58.1l-110.55-85.44a331.25 331.25 0 0 0 81.25-102.07 32.35 32.35 0 0 0 0-29.19C550.29 135.59 442.93 64 320 64a308.15 308.15 0 0 0-147.32 37.7L45.46 3.37A16 16 0 0 0 23 6.18L3.37 31.45A16 16 0 0 0 6.18 53.9l588.36 454.73a16 16 0 0 0 22.46-2.81l19.64-25.27a16 16 0 0 0-2.82-22.45zm-183.72-142l-39.3-30.38A94.75 94.75 0 0 0 416 256a94.76 94.76 0 0 0-121.31-92.21A47.65 47.65 0 0 1 304 192a46.64 46.64 0 0 1-1.54 10l-73.61-56.89A142.31 142.31 0 0 1 320 112a143.92 143.92 0 0 1 144 144c0 21.63-5.29 41.79-13.9 60.11z"></path></svg>
|
||||
|
Before Width: | Height: | Size: 974 B |
@ -1 +0,0 @@
|
||||
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="eye" class="svg-inline--fa fa-eye fa-w-18" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><path fill="currentColor" d="M572.52 241.4C518.29 135.59 410.93 64 288 64S57.68 135.64 3.48 241.41a32.35 32.35 0 0 0 0 29.19C57.71 376.41 165.07 448 288 448s230.32-71.64 284.52-177.41a32.35 32.35 0 0 0 0-29.19zM288 400a144 144 0 1 1 144-144 143.93 143.93 0 0 1-144 144zm0-240a95.31 95.31 0 0 0-25.31 3.79 47.85 47.85 0 0 1-66.9 66.9A95.78 95.78 0 1 0 288 160z"></path></svg>
|
||||
|
Before Width: | Height: | Size: 557 B |
@ -1,11 +0,0 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 448 512"
|
||||
aria-labelby="title"
|
||||
width="1em"
|
||||
height="1em"
|
||||
fill="currentColor"
|
||||
>
|
||||
<title id="title">Scissors</title>
|
||||
<path d="M278.06 256L444.48 89.57c4.69-4.69 4.69-12.29 0-16.97-32.8-32.8-85.99-32.8-118.79 0L210.18 188.12l-24.86-24.86c4.31-10.92 6.68-22.81 6.68-35.26 0-53.02-42.98-96-96-96S0 74.98 0 128s42.98 96 96 96c4.54 0 8.99-.32 13.36-.93L142.29 256l-32.93 32.93c-4.37-.61-8.83-.93-13.36-.93-53.02 0-96 42.98-96 96s42.98 96 96 96 96-42.98 96-96c0-12.45-2.37-24.34-6.68-35.26l24.86-24.86L325.69 439.4c32.8 32.8 85.99 32.8 118.79 0 4.69-4.68 4.69-12.28 0-16.97L278.06 256zM96 160c-17.64 0-32-14.36-32-32s14.36-32 32-32 32 14.36 32 32-14.36 32-32 32zm0 256c-17.64 0-32-14.36-32-32s14.36-32 32-32 32 14.36 32 32-14.36 32-32 32z"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 807 B |
@ -1,21 +0,0 @@
|
||||
<svg
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
viewBox="0 0 512 512"
|
||||
aria-labelby="title"
|
||||
width="1em"
|
||||
height="1em"
|
||||
fill="currentColor"
|
||||
>
|
||||
<title id="title">Sphere</title>
|
||||
<path d="M256,0C114.837,0,0,114.837,0,256s114.837,256,256,256s256-114.837,256-256S397.163,0,256,0z M256,21.333
|
||||
c129.387,0,234.667,105.28,234.667,234.667c0,40.363-96.384,85.333-234.667,85.333c-28.629,0-55.339-2.005-79.893-5.44
|
||||
c-3.435-24.555-5.44-51.264-5.44-79.893C170.667,117.717,215.637,21.333,256,21.333z M208.512,26.176
|
||||
C173.184,67.435,149.333,153.707,149.333,256c0,26.688,1.621,52.309,4.651,76.309C72.683,317.205,21.333,285.419,21.333,256
|
||||
C21.333,142.869,101.824,48.192,208.512,26.176z M26.176,303.488c27.072,23.168,73.429,41.429,131.136,51.2
|
||||
c9.792,57.707,28.032,104.064,51.2,131.136C117.163,466.965,45.035,394.837,26.176,303.488z M256,490.667
|
||||
c-29.419,0-61.205-51.349-76.309-132.651c24,3.029,49.621,4.651,76.309,4.651c102.293,0,188.565-23.851,229.824-59.179
|
||||
C463.808,410.176,369.131,490.667,256,490.667z"/>
|
||||
</svg>
|
||||
<!-- style="enable-background:new 0 0 512 512;" xml:space="preserve" -->
|
||||
|
Before Width: | Height: | Size: 1.1 KiB |
@ -1,53 +0,0 @@
|
||||
import './Range.css';
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
class Range extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { value: props.value || 0 };
|
||||
}
|
||||
|
||||
handleChange = event => {
|
||||
this.setState({ value: event.target.value });
|
||||
if (this.props.onChange) this.props.onChange(event);
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<>
|
||||
<input
|
||||
type="range"
|
||||
value={this.state.value}
|
||||
min={this.props.min}
|
||||
max={this.props.max}
|
||||
step={this.props.step || 1}
|
||||
onChange={this.handleChange}
|
||||
id={this.props.id}
|
||||
className="range"
|
||||
/>
|
||||
{this.props.showPercentage && <span>{`${this.state.value}%`}</span>}
|
||||
{this.props.showValue && <span>{this.state.value}</span>}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Range.propTypes = {
|
||||
value: PropTypes.number,
|
||||
min: PropTypes.number.isRequired,
|
||||
max: PropTypes.number.isRequired,
|
||||
step: PropTypes.number,
|
||||
id: PropTypes.string,
|
||||
onChange: PropTypes.func,
|
||||
showPercentage: PropTypes.bool,
|
||||
showValue: PropTypes.bool,
|
||||
};
|
||||
|
||||
Range.defaultProps = {
|
||||
showPercentage: false,
|
||||
showValue: false,
|
||||
};
|
||||
|
||||
export { Range };
|
||||
@ -1,39 +0,0 @@
|
||||
import './TextArea.css';
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
class TextArea extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { value: this.props.value };
|
||||
}
|
||||
|
||||
handleChange = event => {
|
||||
this.setState({ value: event.target.value });
|
||||
if (this.props.onChange) this.props.onChange();
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<textarea
|
||||
className="textarea-ohif"
|
||||
value={this.state.value}
|
||||
Rows={this.props.Rows}
|
||||
cols={this.props.cols}
|
||||
onChange={this.handleChange}
|
||||
id={this.props.id}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
TextArea.propTypes = {
|
||||
value: PropTypes.string,
|
||||
Rows: PropTypes.number,
|
||||
cols: PropTypes.number,
|
||||
id: PropTypes.string,
|
||||
onChange: PropTypes.func,
|
||||
};
|
||||
|
||||
export { TextArea };
|
||||
@ -1,28 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export function ViewportLoadingIndicator(props) {
|
||||
return (
|
||||
<div
|
||||
className="loading-indicator"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
margin: 'auto',
|
||||
textAlign: 'center',
|
||||
pointerEvents: 'none',
|
||||
}}
|
||||
>
|
||||
Loading {props.percentComplete}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ViewportLoadingIndicator.propTypes = {
|
||||
percentComplete: PropTypes.number,
|
||||
};
|
||||
|
||||
export default ViewportLoadingIndicator;
|
||||
@ -1,287 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
|
||||
import { format } from 'date-fns';
|
||||
import { Button, Icon, EmptyStudies } from '@ohif/ui';
|
||||
|
||||
const getGridColClass = (filtersMeta, name) => {
|
||||
const filter = filtersMeta.find((filter) => filter.name === name);
|
||||
return (filter && filter.gridCol && `w-${filter.gridCol}/24`) || '';
|
||||
};
|
||||
|
||||
const StudyTableRow = (props) => {
|
||||
const {
|
||||
AccessionNumber,
|
||||
Modalities,
|
||||
Instances,
|
||||
StudyDescription,
|
||||
PatientId,
|
||||
PatientName,
|
||||
StudyDate,
|
||||
series,
|
||||
filtersMeta,
|
||||
} = props;
|
||||
|
||||
const [isOpened, setIsOpened] = useState(false);
|
||||
const toggleRow = () => setIsOpened(!isOpened);
|
||||
const ChevronIconName = isOpened ? 'chevron-down' : 'chevron-right';
|
||||
const tdClasses = [
|
||||
'px-4 py-2 text-base',
|
||||
{ 'border-b border-secondary-light': !isOpened },
|
||||
];
|
||||
return (
|
||||
<>
|
||||
<tr>
|
||||
<td
|
||||
className={classnames('border-0 p-0', {
|
||||
'border-b border-secondary-light bg-primary-dark': isOpened,
|
||||
})}
|
||||
>
|
||||
<div
|
||||
className={classnames('w-full transition duration-300', {
|
||||
'border border-primary-light rounded overflow-hidden mb-2 hover:border-secondary-light': isOpened,
|
||||
})}
|
||||
>
|
||||
<table className={classnames('w-full p-4')}>
|
||||
<tbody>
|
||||
<tr
|
||||
className={classnames(
|
||||
'cursor-pointer hover:bg-secondary-main transition duration-300 bg-black',
|
||||
{
|
||||
'bg-primary-dark': !isOpened,
|
||||
},
|
||||
{ 'bg-secondary-dark': isOpened }
|
||||
)}
|
||||
onClick={toggleRow}
|
||||
>
|
||||
<td
|
||||
className={classnames(
|
||||
...tdClasses,
|
||||
getGridColClass(filtersMeta, 'patientName')
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-row items-center pl-1">
|
||||
<Icon name={ChevronIconName} className="mr-4" />
|
||||
{PatientName}
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
className={classnames(
|
||||
...tdClasses,
|
||||
getGridColClass(filtersMeta, 'mrn')
|
||||
)}
|
||||
>
|
||||
{PatientId}
|
||||
</td>
|
||||
<td
|
||||
className={classnames(
|
||||
...tdClasses,
|
||||
getGridColClass(filtersMeta, 'studyDate')
|
||||
)}
|
||||
>
|
||||
{format(StudyDate, 'MMM-DD-YYYY')}
|
||||
</td>
|
||||
<td
|
||||
className={classnames(
|
||||
...tdClasses,
|
||||
getGridColClass(filtersMeta, 'description')
|
||||
)}
|
||||
>
|
||||
{StudyDescription}
|
||||
</td>
|
||||
<td
|
||||
className={classnames(
|
||||
...tdClasses,
|
||||
getGridColClass(filtersMeta, 'modality')
|
||||
)}
|
||||
>
|
||||
{Modalities}
|
||||
</td>
|
||||
<td
|
||||
className={classnames(
|
||||
...tdClasses,
|
||||
getGridColClass(filtersMeta, 'accession')
|
||||
)}
|
||||
>
|
||||
{AccessionNumber}
|
||||
</td>
|
||||
<td
|
||||
className={classnames(
|
||||
...tdClasses,
|
||||
getGridColClass(filtersMeta, 'instances')
|
||||
)}
|
||||
>
|
||||
<Icon
|
||||
name="series-active"
|
||||
className={classnames('inline-flex mr-2', {
|
||||
'text-primary-active': isOpened,
|
||||
'text-secondary-light': !isOpened,
|
||||
})}
|
||||
/>
|
||||
{Instances}
|
||||
</td>
|
||||
</tr>
|
||||
{isOpened && (
|
||||
<tr
|
||||
className={classnames('bg-black max-h-0 overflow-hidden')}
|
||||
>
|
||||
<td colSpan="7" className="py-4 pl-12 pr-2">
|
||||
<div className="block">
|
||||
<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>
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell size="normal">Description</TableCell>
|
||||
<TableCell size="small">Series</TableCell>
|
||||
<TableCell size="small">Modality</TableCell>
|
||||
<TableCell size="normal">Instances</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
|
||||
<TableBody>
|
||||
{series.map((seriesItem, i) => (
|
||||
<TableRow key={i}>
|
||||
<TableCell size="normal">
|
||||
Patient Protocol
|
||||
</TableCell>
|
||||
<TableCell size="small">
|
||||
{seriesItem.SeriesNumber}
|
||||
</TableCell>
|
||||
<TableCell size="small">
|
||||
{seriesItem.Modality}
|
||||
</TableCell>
|
||||
<TableCell size="normal">
|
||||
{seriesItem.instances.length}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
StudyTableRow.propTypes = {
|
||||
AccessionNumber: PropTypes.string.isRequired,
|
||||
Modalities: PropTypes.string.isRequired,
|
||||
Instances: PropTypes.number.isRequired,
|
||||
PatientId: PropTypes.string.isRequired,
|
||||
PatientName: PropTypes.string.isRequired,
|
||||
StudyDescription: PropTypes.string.isRequired,
|
||||
StudyDate: PropTypes.string.isRequired,
|
||||
series: PropTypes.array.isRequired,
|
||||
};
|
||||
|
||||
const StudyListTable = ({ studies, numOfStudies, filtersMeta }) => {
|
||||
const renderTable = () => {
|
||||
return (
|
||||
<table className="w-full text-white">
|
||||
<tbody>
|
||||
{studies.map((study, i) => (
|
||||
<StudyTableRow
|
||||
key={i}
|
||||
AccessionNumber={study.AccessionNumber || ''}
|
||||
Modalities={study.Modalities || ''}
|
||||
Instances={study.Instances || ''}
|
||||
StudyDescription={study.StudyDescription || ''}
|
||||
PatientId={study.PatientId || ''}
|
||||
PatientName={study.PatientName || ''}
|
||||
StudyDate={study.StudyDate || ''}
|
||||
series={study.series || []}
|
||||
filtersMeta={filtersMeta}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
};
|
||||
|
||||
const renderEmpty = () => {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center pt-48">
|
||||
<EmptyStudies />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="bg-black">
|
||||
<div className="container m-auto relative">
|
||||
{numOfStudies > 0 && renderTable()}
|
||||
{numOfStudies === 0 && renderEmpty()}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
StudyListTable.propTypes = {
|
||||
studies: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
AccessionNumber: PropTypes.string.isRequired,
|
||||
Modalities: PropTypes.string.isRequired,
|
||||
Instances: PropTypes.number.isRequired,
|
||||
PatientId: PropTypes.string.isRequired,
|
||||
PatientName: PropTypes.string.isRequired,
|
||||
StudyDescription: PropTypes.string.isRequired,
|
||||
StudyDate: PropTypes.string.isRequired,
|
||||
series: PropTypes.array.isRequired,
|
||||
})
|
||||
).isRequired,
|
||||
numOfStudies: PropTypes.number.isRequired,
|
||||
};
|
||||
|
||||
export default StudyListTable;
|
||||
@ -4,6 +4,7 @@ menu: Views
|
||||
route: examples/viewer
|
||||
---
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Playground, Props } from 'docz';
|
||||
import {
|
||||
NavBar,
|
||||
@ -51,6 +52,7 @@ import { tabs } from './studyBrowserMockData';
|
||||
onClick: (id) => setActiveMeasurementItem((s) => (s === id ? null : id)),
|
||||
onEdit: (id) => alert(`Edit: ${id}`),
|
||||
};
|
||||
const [activeViewportIndex, setActiveViewportIndex] = useState(0);
|
||||
return (
|
||||
<DragAndDropProvider>
|
||||
<Header />
|
||||
|
||||
@ -4,7 +4,7 @@ const studyWithSR = {
|
||||
description: 'CHEST/ABD/PELVIS W/CONTRAST',
|
||||
numInstances: 902,
|
||||
modalities: 'CT,SR',
|
||||
thumbnails: [
|
||||
displaySets: [
|
||||
{
|
||||
displaySetInstanceUid: 'f69f6asdasd48c-223e-db7f-c4af-b8906641a66e',
|
||||
description: 'Multiple line image series description lorem sit',
|
||||
|
||||
@ -191,8 +191,8 @@ class OHIFStandaloneViewer extends Component {
|
||||
{match === null ? (
|
||||
<></>
|
||||
) : (
|
||||
<Component match={match} location={this.props.location} />
|
||||
)}
|
||||
<Component match={match} location={this.props.location} />
|
||||
)}
|
||||
</CSSTransition>
|
||||
)}
|
||||
</Route>
|
||||
|
||||
@ -37,9 +37,6 @@ const mapStateToProps = state => {
|
||||
};
|
||||
};
|
||||
|
||||
const ConnectedViewportGrid = connect(
|
||||
mapStateToProps,
|
||||
null
|
||||
)(ViewportGrid);
|
||||
const ConnectedViewportGrid = connect(mapStateToProps, null)(ViewportGrid);
|
||||
|
||||
export default ConnectedViewportGrid;
|
||||
|
||||
@ -12,7 +12,7 @@ import EmptyViewport from './EmptyViewport.js';
|
||||
|
||||
const { loadAndCacheDerivedDisplaySets } = utils;
|
||||
|
||||
const ViewportGrid = function (props) {
|
||||
const ViewportGrid = function(props) {
|
||||
const {
|
||||
activeViewportIndex,
|
||||
availablePlugins,
|
||||
@ -24,7 +24,7 @@ const ViewportGrid = function (props) {
|
||||
studies,
|
||||
viewportData,
|
||||
children,
|
||||
isStudyLoaded
|
||||
isStudyLoaded,
|
||||
} = props;
|
||||
|
||||
const rowSize = 100 / numRows;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user