* ohif-323: add cineplayer flow * ohif-323: improve toolbar flow of cine * ohif-323: improve cine styles * ohif-323: use toggle instead of set * ohif-323: add compatibility comment * ohif-323: move cine logic to command * ohif-323: cr update * ohif-323: remove typo and update svg title * ohif-323: remove log * ohif-323: update props and remove dead code Co-authored-by: Danny Brown <danny.ri.brown@gmail.com>
362 lines
11 KiB
JavaScript
362 lines
11 KiB
JavaScript
import React, { useState, useRef, useEffect } from 'react';
|
|
import PropTypes from 'prop-types';
|
|
import classnames from 'classnames';
|
|
import { Icon, ButtonGroup, Button, Tooltip, CinePlayer } from '../';
|
|
import useOnClickOutside from '../../utils/useOnClickOutside';
|
|
|
|
const classes = {
|
|
infoHeader: 'text-base text-primary-light',
|
|
infoText: 'text-base text-white max-w-24 truncate',
|
|
firstRow: 'flex flex-col',
|
|
row: 'flex flex-col ml-4',
|
|
};
|
|
|
|
const ViewportActionBar = ({
|
|
studyData,
|
|
showNavArrows,
|
|
showCine,
|
|
cineProps,
|
|
showPatientInfo: patientInfoVisibility,
|
|
onSeriesChange,
|
|
onDoubleClick,
|
|
}) => {
|
|
const [showPatientInfo, setShowPatientInfo] = useState(patientInfoVisibility);
|
|
|
|
// TODO -> Remake this component with a bunch of generic slots that can be filled,
|
|
// Its not generic at all, isTracked etc shouldn't be parts of this component.
|
|
// It shouldn't care that a tracking mode or SR exists.
|
|
// Things like the right/left buttons should be made into smaller
|
|
// Components you can compose.
|
|
// OHIF-200 ticket.
|
|
|
|
const {
|
|
label,
|
|
isTracked,
|
|
isLocked,
|
|
modality,
|
|
studyDate,
|
|
currentSeries,
|
|
seriesDescription,
|
|
patientInformation,
|
|
} = studyData;
|
|
|
|
const {
|
|
patientName,
|
|
patientSex,
|
|
patientAge,
|
|
MRN,
|
|
thickness,
|
|
spacing,
|
|
scanner,
|
|
} = patientInformation;
|
|
|
|
const onPatientInfoClick = () => setShowPatientInfo(!showPatientInfo);
|
|
const closePatientInfo = () => setShowPatientInfo(false);
|
|
|
|
const showPatientInfoRef = useRef(null);
|
|
const clickOutsideListener = useOnClickOutside(
|
|
showPatientInfoRef,
|
|
closePatientInfo
|
|
);
|
|
|
|
useEffect(() => {
|
|
if (showPatientInfo) {
|
|
clickOutsideListener.add();
|
|
} else {
|
|
clickOutsideListener.remove();
|
|
}
|
|
|
|
return () => clickOutsideListener.remove();
|
|
}, [clickOutsideListener, showPatientInfo]);
|
|
|
|
const renderIconStatus = () => {
|
|
if (modality === 'SR') {
|
|
const TooltipMessage = isLocked
|
|
? () => (
|
|
<div>
|
|
This SR is locked. <br />
|
|
Measurements cannot be duplicated.
|
|
</div>
|
|
)
|
|
: () => <div>This SR is unlocked.</div>;
|
|
return (
|
|
<>
|
|
<Tooltip content={<TooltipMessage />} position="bottom-left">
|
|
<div className="relative flex p-1 border rounded cursor-default border-primary-light">
|
|
<span className="text-sm font-bold leading-none text-primary-light">
|
|
SR
|
|
</span>
|
|
{isLocked && (
|
|
<Icon
|
|
name="lock"
|
|
className="absolute w-3 text-white"
|
|
style={{ top: -6, right: -6 }}
|
|
/>
|
|
)}
|
|
</div>
|
|
</Tooltip>
|
|
</>
|
|
);
|
|
}
|
|
|
|
const trackedIcon = isTracked ? 'tracked' : 'dotted-circle';
|
|
|
|
return (
|
|
<div className="relative">
|
|
<Tooltip
|
|
position="bottom-left"
|
|
content={
|
|
<div className="flex py-2">
|
|
<div className="flex pt-1">
|
|
<Icon name="info-link" className="w-4 text-primary-main" />
|
|
</div>
|
|
<div className="flex ml-4">
|
|
<span className="text-base text-common-light">
|
|
{isTracked ? (
|
|
<>
|
|
Series is
|
|
<span className="font-bold text-white"> tracked</span> and
|
|
can be viewed <br /> in the measurement panel
|
|
</>
|
|
) : (
|
|
<>
|
|
Measurements for
|
|
<span className="font-bold text-white"> untracked </span>
|
|
series <br /> will not be shown in the <br /> measurements
|
|
panel
|
|
</>
|
|
)}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
}
|
|
>
|
|
<Icon name={trackedIcon} className="w-6 text-primary-light" />
|
|
</Tooltip>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<div
|
|
className="flex flex-wrap items-center p-2 border-b select-none border-primary-light -mt-2"
|
|
onDoubleClick={onDoubleClick}
|
|
>
|
|
<div className="flex flex-grow min-w-48 flex-1 mt-2">
|
|
<div className="flex items-center">
|
|
{renderIconStatus()}
|
|
<span className="ml-2 text-white text-large">{label}</span>
|
|
</div>
|
|
<div className="flex flex-col justify-start ml-4">
|
|
<div className="flex">
|
|
<span className="text-base text-white">{studyDate}</span>
|
|
<span className="pl-2 ml-2 text-base border-l border-primary-light text-primary-light">
|
|
S: {currentSeries}
|
|
</span>
|
|
</div>
|
|
<div className="flex">
|
|
{/* TODO:
|
|
This is tricky. Our "no-wrap" in truncate means this has a hard
|
|
length. The overflow forces ellipse. If we don't set max width
|
|
appropriately, this causes the ActionBar to overflow.
|
|
Can clean up by setting percentage widths + calc on parent
|
|
containers
|
|
*/}
|
|
<p className="text-base truncate max-w-40 text-primary-light">
|
|
{seriesDescription}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{showNavArrows && !showCine && (
|
|
<div className="mt-2">
|
|
<ButtonGroup>
|
|
<Button
|
|
size="initial"
|
|
className="px-2 py-1"
|
|
onClick={() => onSeriesChange('left')}
|
|
>
|
|
<Icon name="chevron-left" className="w-4 text-white" />
|
|
</Button>
|
|
<Button
|
|
size="initial"
|
|
className="px-2 py-1"
|
|
onClick={() => onSeriesChange('right')}
|
|
>
|
|
<Icon name="chevron-right" className="w-4 text-white" />
|
|
</Button>
|
|
</ButtonGroup>
|
|
</div>
|
|
)}
|
|
{showCine && !showNavArrows && (
|
|
<div className="mt-2 min-w-48 max-w-48 mr-auto">
|
|
<CinePlayer {...cineProps} />
|
|
</div>
|
|
)}
|
|
<div className="flex h-8 ml-4 mr-2 mt-2" onClick={onPatientInfoClick}>
|
|
<PatientInfo
|
|
showPatientInfoRef={showPatientInfoRef}
|
|
isOpen={showPatientInfo}
|
|
patientName={patientName}
|
|
patientSex={patientSex}
|
|
patientAge={patientAge}
|
|
MRN={MRN}
|
|
thickness={thickness}
|
|
spacing={spacing}
|
|
scanner={scanner}
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
ViewportActionBar.propTypes = {
|
|
onSeriesChange: PropTypes.func.isRequired,
|
|
showNavArrows: PropTypes.bool,
|
|
showCine: PropTypes.bool,
|
|
cineProps: PropTypes.object,
|
|
showPatientInfo: PropTypes.bool,
|
|
studyData: PropTypes.shape({
|
|
label: PropTypes.string.isRequired,
|
|
isTracked: PropTypes.bool.isRequired,
|
|
isLocked: PropTypes.bool.isRequired,
|
|
studyDate: PropTypes.string.isRequired,
|
|
currentSeries: PropTypes.number.isRequired,
|
|
seriesDescription: PropTypes.string.isRequired,
|
|
modality: PropTypes.string.isRequired,
|
|
patientInformation: PropTypes.shape({
|
|
patientName: PropTypes.string.isRequired,
|
|
patientSex: PropTypes.string.isRequired,
|
|
patientAge: PropTypes.string.isRequired,
|
|
MRN: PropTypes.string.isRequired,
|
|
thickness: PropTypes.string.isRequired,
|
|
spacing: PropTypes.string.isRequired,
|
|
scanner: PropTypes.string.isRequired,
|
|
}),
|
|
}).isRequired,
|
|
};
|
|
|
|
ViewportActionBar.defaultProps = {
|
|
cineProps: {},
|
|
showCine: false,
|
|
showNavArrows: true,
|
|
showPatientInfo: false,
|
|
};
|
|
|
|
function PatientInfo({
|
|
patientName,
|
|
patientSex,
|
|
patientAge,
|
|
MRN,
|
|
thickness,
|
|
spacing,
|
|
scanner,
|
|
isOpen,
|
|
showPatientInfoRef,
|
|
}) {
|
|
while (patientAge.charAt(0) === '0') {
|
|
patientAge = patientAge.substr(1);
|
|
}
|
|
|
|
return (
|
|
<div ref={showPatientInfoRef}>
|
|
<Tooltip
|
|
isSticky
|
|
isDisabled={!isOpen}
|
|
position="bottom-right"
|
|
content={
|
|
isOpen && (
|
|
<div className="flex py-2">
|
|
<div className="flex pt-1">
|
|
<Icon name="info-link" className="w-4 text-primary-main" />
|
|
</div>
|
|
<div className="flex flex-col ml-2">
|
|
<span
|
|
className="text-base font-bold text-white"
|
|
title={patientName}
|
|
>
|
|
{patientName}
|
|
</span>
|
|
<div className="flex pb-4 mt-4 mb-4 border-b border-secondary-main">
|
|
<div className={classnames(classes.firstRow)}>
|
|
<span className={classnames(classes.infoHeader)}>Sex</span>
|
|
<span
|
|
className={classnames(classes.infoText)}
|
|
title={patientSex}
|
|
>
|
|
{patientSex}
|
|
</span>
|
|
</div>
|
|
<div className={classnames(classes.row)}>
|
|
<span className={classnames(classes.infoHeader)}>Age</span>
|
|
<span
|
|
className={classnames(classes.infoText)}
|
|
title={patientAge}
|
|
>
|
|
{patientAge}
|
|
</span>
|
|
</div>
|
|
<div className={classnames(classes.row)}>
|
|
<span className={classnames(classes.infoHeader)}>MRN</span>
|
|
<span className={classnames(classes.infoText)} title={MRN}>
|
|
{MRN}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<div className="flex">
|
|
<div className={classnames(classes.firstRow)}>
|
|
<span className={classnames(classes.infoHeader)}>
|
|
Thickness
|
|
</span>
|
|
<span
|
|
className={classnames(classes.infoText)}
|
|
title={thickness}
|
|
>
|
|
{thickness}
|
|
</span>
|
|
</div>
|
|
<div className={classnames(classes.row)}>
|
|
<span className={classnames(classes.infoHeader)}>
|
|
Spacing
|
|
</span>
|
|
<span
|
|
className={classnames(classes.infoText)}
|
|
title={spacing}
|
|
>
|
|
{spacing}
|
|
</span>
|
|
</div>
|
|
<div className={classnames(classes.row)}>
|
|
<span className={classnames(classes.infoHeader)}>
|
|
Scanner
|
|
</span>
|
|
<span
|
|
className={classnames(classes.infoText)}
|
|
title={scanner}
|
|
>
|
|
{scanner}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
>
|
|
<div className="relative flex justify-end cursor-pointer">
|
|
<div className="relative">
|
|
<Icon name="profile" className="w-5 text-white" />
|
|
<Icon
|
|
name="info-link"
|
|
className="absolute w-5 text-white bg-black"
|
|
style={{ right: -7, bottom: -10 }}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</Tooltip>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default ViewportActionBar;
|