Merge branch 'feat/v2-main' of https://github.com/OHIF/Viewers into fix/OHIF-37-thumbnailDoubleClick
This commit is contained in:
commit
4b8dcef956
48 files changed
+1156
-363
No files matched your search
@@ -113,7 +113,7 @@ const IconButton = ({
|
||||
};
|
||||
|
||||
IconButton.defaultProps = {
|
||||
onClick: () => {},
|
||||
onClick: () => { },
|
||||
color: 'default',
|
||||
disabled: false,
|
||||
fullWidth: false,
|
||||
|
||||
@@ -1,16 +1,35 @@
|
||||
import React from 'react';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import classnames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Button, Icon } from '@ohif/ui';
|
||||
|
||||
const Notification = ({ type, message, actions, onSubmit }) => {
|
||||
const Notification = ({ type, message, actions, onSubmit, onOutsideClick }) => {
|
||||
const notificationRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
const notificationElement = notificationRef.current;
|
||||
const handleClick = function(event) {
|
||||
const isClickInside = notificationElement.contains(event.target);
|
||||
|
||||
if (!isClickInside) {
|
||||
onOutsideClick();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('mousedown', handleClick);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClick);
|
||||
};
|
||||
}, [onOutsideClick]);
|
||||
|
||||
const iconsByType = {
|
||||
error: {
|
||||
icon: 'info',
|
||||
color: 'text-red-700',
|
||||
},
|
||||
warning: {
|
||||
icon: 'info',
|
||||
icon: 'notificationwarning-diamond',
|
||||
color: 'text-yellow-500',
|
||||
},
|
||||
info: {
|
||||
@@ -35,7 +54,10 @@ const Notification = ({ type, message, actions, onSubmit }) => {
|
||||
const { icon, color } = getIconData();
|
||||
|
||||
return (
|
||||
<div className="flex flex-col p-2 mx-2 mt-2 rounded bg-common-bright">
|
||||
<div
|
||||
ref={notificationRef}
|
||||
className="flex flex-col p-2 mx-2 mt-2 rounded bg-common-bright"
|
||||
>
|
||||
<div className="flex flex-grow">
|
||||
<Icon name={icon} className={classnames('w-5', color)} />
|
||||
<span className="ml-2 text-base text-black">{message}</span>
|
||||
@@ -65,6 +87,7 @@ const Notification = ({ type, message, actions, onSubmit }) => {
|
||||
|
||||
Notification.defaultProps = {
|
||||
type: 'info',
|
||||
onOutsideClick: () => {},
|
||||
};
|
||||
|
||||
Notification.propTypes = {
|
||||
@@ -78,6 +101,8 @@ Notification.propTypes = {
|
||||
})
|
||||
).isRequired,
|
||||
onSubmit: PropTypes.func.isRequired,
|
||||
/** Can be used as a callback to dismiss the notification for clicks that occur outside of it */
|
||||
onOutsideClick: PropTypes.func,
|
||||
};
|
||||
|
||||
export default Notification;
|
||||
@@ -0,0 +1,115 @@
|
||||
/* TODO: Create tailwind styles for this component */
|
||||
.sb-topLeft {
|
||||
@apply top-0 left-0 bottom-auto right-auto;
|
||||
}
|
||||
|
||||
.sb-topCenter {
|
||||
transform: translateX(-50%);
|
||||
@apply top-0 bottom-auto left-1/2;
|
||||
}
|
||||
|
||||
.sb-topRight {
|
||||
@apply right-0 top-0 left-auto bottom-auto;
|
||||
}
|
||||
|
||||
.sb-bottomLeft {
|
||||
@apply right-auto left-0 bottom-0 top-auto;
|
||||
}
|
||||
|
||||
.sb-bottomCenter {
|
||||
@apply top-auto bottom-0 left-1/2;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.sb-bottomRight {
|
||||
margin: 10px 0 0;
|
||||
@apply top-auto bottom-0 left-auto right-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 {
|
||||
text-shadow: none;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
right: 5px;
|
||||
top: 5px;
|
||||
@apply overflow-hidden opacity-100 rounded-full p-1 bg-white cursor-pointer absolute text-center duration-300 transition-all ease-in-out;
|
||||
}
|
||||
|
||||
.sb-closeBtn:hover {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.sb-closeIcon {
|
||||
@apply w-full relative overflow-hidden h-full block leading-none;
|
||||
}
|
||||
|
||||
.sb-closeIcon:after,
|
||||
.sb-closeIcon:before {
|
||||
content: ' ';
|
||||
height: 2px;
|
||||
width: 12px;
|
||||
@apply duration-300 transition-all ease-in-out block bg-black opacity-100 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 {
|
||||
@apply break-normal text-lg font-bold;
|
||||
}
|
||||
|
||||
.sb-message {
|
||||
@apply break-normal text-base;
|
||||
}
|
||||
|
||||
.sb-item {
|
||||
animation: fadein 1s;
|
||||
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);
|
||||
@apply relative p-5 text-white overflow-hidden rounded-md transition-height ease-in-out duration-300;
|
||||
}
|
||||
|
||||
@keyframes fadein {
|
||||
from {
|
||||
top: 30px;
|
||||
@apply opacity-0;
|
||||
}
|
||||
to {
|
||||
@apply opacity-100 top-0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Internet Explorer */
|
||||
@-ms-keyframes fadein {
|
||||
from {
|
||||
top: 30px;
|
||||
@apply opacity-0;
|
||||
}
|
||||
to {
|
||||
@apply opacity-100;
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
@@ -2,16 +2,18 @@ import React from 'react';
|
||||
import SnackbarItem from './SnackbarItem';
|
||||
import { useSnackbar } from '../../contextProviders';
|
||||
|
||||
import './Snackbar.css';
|
||||
|
||||
const SnackbarContainer = () => {
|
||||
const { snackbarItems, hide } = useSnackbar();
|
||||
|
||||
const renderItem = item => {
|
||||
return <SnackbarItem key={item.itemId} options={item} onClose={hide} />;
|
||||
};
|
||||
|
||||
if (!snackbarItems) {
|
||||
return null;
|
||||
}
|
||||
const renderItem = item => (
|
||||
<SnackbarItem
|
||||
key={item.itemId}
|
||||
options={item}
|
||||
onClose={hide}
|
||||
/>
|
||||
);
|
||||
|
||||
const renderItems = () => {
|
||||
const items = {
|
||||
@@ -23,11 +25,9 @@ const SnackbarContainer = () => {
|
||||
bottomRight: [],
|
||||
};
|
||||
|
||||
snackbarItems.map(item => {
|
||||
items[item.position].push(item);
|
||||
});
|
||||
snackbarItems.forEach(item => items[item.position].push(item));
|
||||
|
||||
return (
|
||||
return snackbarItems && (
|
||||
<div>
|
||||
{Object.keys(items).map(pos => {
|
||||
if (!items[pos].length) {
|
||||
@@ -35,7 +35,7 @@ const SnackbarContainer = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={pos} className={`sb-container sb-${pos}`}>
|
||||
<div key={pos} className={`fixed z-50 p-6 box-border h-auto sb-${pos}`}>
|
||||
{items[pos].map((item, index) => (
|
||||
<div key={item.id + index}>{renderItem(item)}</div>
|
||||
))}
|
||||
|
||||
@@ -1,25 +1,38 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import SnackbarTypes from './SnackbarTypes';
|
||||
|
||||
const SnackbarItem = ({ options, onClose }) => {
|
||||
const handleClose = () => {
|
||||
onClose(options.id);
|
||||
};
|
||||
const handleClose = () => onClose(options.id);
|
||||
|
||||
useEffect(() => {
|
||||
if (options.autoClose) {
|
||||
setTimeout(() => {
|
||||
handleClose();
|
||||
}, options.duration);
|
||||
setTimeout(() => handleClose(), options.duration);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const typeClasses = {
|
||||
[SnackbarTypes.INFO]: 'bg-primary-active',
|
||||
[SnackbarTypes.WARNING]: 'bg-yellow-600',
|
||||
[SnackbarTypes.SUCCESS]: 'bg-green-600',
|
||||
[SnackbarTypes.ERROR]: 'bg-red-600'
|
||||
};
|
||||
|
||||
const hidden = 'duration-300 transition-all ease-in-out h-0 opacity-0 pt-0 mb-0 pb-0';
|
||||
|
||||
return (
|
||||
<div>
|
||||
<span onClick={handleClose}>
|
||||
<span>x</span>
|
||||
<div
|
||||
className={classNames(
|
||||
`${options.visible ? '' : hidden} sb-item`,
|
||||
typeClasses[options.type]
|
||||
)}
|
||||
>
|
||||
<span className="sb-closeBtn" onClick={handleClose}>
|
||||
<span className="sb-closeIcon">x</span>
|
||||
</span>
|
||||
{options.title && <div>{options.title}</div>}
|
||||
{options.message && <div>{options.message}</div>}
|
||||
{options.title && <div className="sb-title">{options.title}</div>}
|
||||
{options.message && <div className="sb-message">{options.message}</div>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -10,6 +10,7 @@ import blurHandlerListener from '../../utils/blurHandlerListener';
|
||||
*
|
||||
*/
|
||||
const Thumbnail = ({
|
||||
displaySetInstanceUID,
|
||||
className,
|
||||
imageSrc,
|
||||
imageAltText,
|
||||
@@ -40,6 +41,7 @@ const Thumbnail = ({
|
||||
className,
|
||||
'flex flex-col flex-1 px-3 mb-8 cursor-pointer outline-none group'
|
||||
)}
|
||||
id={`thumbnail-${displaySetInstanceUID}`}
|
||||
onDoubleClick={onClick}
|
||||
role="button"
|
||||
tabIndex="0"
|
||||
@@ -79,6 +81,7 @@ const Thumbnail = ({
|
||||
};
|
||||
|
||||
Thumbnail.propTypes = {
|
||||
displaySetInstanceUID: PropTypes.string.isRequired,
|
||||
className: PropTypes.string,
|
||||
imageSrc: PropTypes.string,
|
||||
/**
|
||||
|
||||
@@ -34,6 +34,7 @@ const ThumbnailList = ({
|
||||
return (
|
||||
<Thumbnail
|
||||
key={displaySetInstanceUID}
|
||||
displaySetInstanceUID={displaySetInstanceUID}
|
||||
dragData={dragData}
|
||||
description={description}
|
||||
seriesNumber={seriesNumber}
|
||||
@@ -49,6 +50,7 @@ const ThumbnailList = ({
|
||||
return (
|
||||
<ThumbnailTracked
|
||||
key={displaySetInstanceUID}
|
||||
displaySetInstanceUID={displaySetInstanceUID}
|
||||
dragData={dragData}
|
||||
description={description}
|
||||
seriesNumber={seriesNumber}
|
||||
@@ -67,6 +69,7 @@ const ThumbnailList = ({
|
||||
<ThumbnailNoImage
|
||||
isActive={isActive}
|
||||
key={displaySetInstanceUID}
|
||||
displaySetInstanceUID={displaySetInstanceUID}
|
||||
dragData={dragData}
|
||||
modality={modality}
|
||||
seriesDate={seriesDate}
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import React, { useRef } from 'react';
|
||||
import classnames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import { useDrag } from 'react-dnd';
|
||||
|
||||
import { Icon } from '@ohif/ui';
|
||||
import blurHandlerListener from '../../utils/blurHandlerListener';
|
||||
|
||||
const ThumbnailNoImage = ({
|
||||
displaySetInstanceUID,
|
||||
description,
|
||||
seriesDate,
|
||||
modality,
|
||||
@@ -31,6 +33,7 @@ const ThumbnailNoImage = ({
|
||||
'flex flex-row flex-1 px-4 py-3 cursor-pointer outline-none border-transparent hover:border-blue-300 focus:border-blue-300 rounded',
|
||||
isActive ? 'border-2 border-primary-light' : 'border'
|
||||
)}
|
||||
id={`thumbnail-${displaySetInstanceUID}`}
|
||||
onDoubleClick={onClick}
|
||||
role="button"
|
||||
tabIndex="0"
|
||||
@@ -54,6 +57,7 @@ const ThumbnailNoImage = ({
|
||||
};
|
||||
|
||||
ThumbnailNoImage.propTypes = {
|
||||
displaySetInstanceUID: PropTypes.string.isRequired,
|
||||
/**
|
||||
* Data the thumbnail should expose to a receiving drop target. Use a matching
|
||||
* `dragData.type` to identify which targets can receive this draggable item.
|
||||
|
||||
@@ -5,6 +5,7 @@ import classnames from 'classnames';
|
||||
import { Icon, Thumbnail, Tooltip } from '@ohif/ui';
|
||||
|
||||
const ThumbnailTracked = ({
|
||||
displaySetInstanceUID,
|
||||
className,
|
||||
imageSrc,
|
||||
imageAltText,
|
||||
@@ -26,6 +27,7 @@ const ThumbnailTracked = ({
|
||||
'flex flex-row flex-1 px-3 py-2 cursor-pointer outline-none',
|
||||
className
|
||||
)}
|
||||
id={`thumbnail-${displaySetInstanceUID}`}
|
||||
>
|
||||
<div className="flex flex-col items-center flex-2">
|
||||
<div
|
||||
|
||||
@@ -39,7 +39,7 @@ const ViewportActionBar = ({
|
||||
scanner,
|
||||
} = patientInformation;
|
||||
|
||||
const onPatientInfoClick = () => setShowPatientInfo(!showPatientInfo)
|
||||
const onPatientInfoClick = () => setShowPatientInfo(!showPatientInfo);
|
||||
|
||||
const renderIconStatus = () => {
|
||||
if (modality === 'SR') {
|
||||
@@ -64,26 +64,26 @@ const ViewportActionBar = ({
|
||||
{!isTracked ? (
|
||||
<Icon name="dotted-circle" className="w-6 text-primary-light" />
|
||||
) : (
|
||||
<Tooltip
|
||||
position="bottom-left"
|
||||
content={
|
||||
<div className="flex py-2">
|
||||
<div className="flex pt-1">
|
||||
<Icon name="info-link" className="w-4 text-primary-main" />
|
||||
</div>
|
||||
<div className="flex ml-4">
|
||||
<span className="text-base text-common-light">
|
||||
Series is
|
||||
<Tooltip
|
||||
position="bottom-left"
|
||||
content={
|
||||
<div className="flex py-2">
|
||||
<div className="flex pt-1">
|
||||
<Icon name="info-link" className="w-4 text-primary-main" />
|
||||
</div>
|
||||
<div className="flex ml-4">
|
||||
<span className="text-base text-common-light">
|
||||
Series is
|
||||
<span className="font-bold text-white"> tracked</span> and
|
||||
can be viewed <br /> in the measurement panel
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Icon name="tracked" className="w-6 text-primary-light" />
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Icon name="tracked" className="w-6 text-primary-light" />
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -196,54 +196,64 @@ function PatientInfo({
|
||||
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">
|
||||
{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)}>
|
||||
{patientSex}
|
||||
</span>
|
||||
</div>
|
||||
<div className={classnames(classes.row)}>
|
||||
<span className={classnames(classes.infoHeader)}>Age</span>
|
||||
<span className={classnames(classes.infoText)}>
|
||||
{patientAge}
|
||||
</span>
|
||||
</div>
|
||||
<div className={classnames(classes.row)}>
|
||||
<span className={classnames(classes.infoHeader)}>MRN</span>
|
||||
<span className={classnames(classes.infoText)}>{MRN}</span>
|
||||
</div>
|
||||
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">
|
||||
<div className={classnames(classes.firstRow)}>
|
||||
<span className={classnames(classes.infoHeader)}>
|
||||
Thickness
|
||||
</span>
|
||||
<span className={classnames(classes.infoText)}>
|
||||
{thickness}
|
||||
</span>
|
||||
<div className="flex flex-col ml-2">
|
||||
<span className="text-base font-bold text-white">
|
||||
{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)}>
|
||||
{patientSex}
|
||||
</span>
|
||||
</div>
|
||||
<div className={classnames(classes.row)}>
|
||||
<span className={classnames(classes.infoHeader)}>Age</span>
|
||||
<span className={classnames(classes.infoText)}>
|
||||
{patientAge}
|
||||
</span>
|
||||
</div>
|
||||
<div className={classnames(classes.row)}>
|
||||
<span className={classnames(classes.infoHeader)}>MRN</span>
|
||||
<span className={classnames(classes.infoText)}>{MRN}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className={classnames(classes.row)}>
|
||||
<span className={classnames(classes.infoHeader)}>Spacing</span>
|
||||
<span className={classnames(classes.infoText)}>{spacing}</span>
|
||||
</div>
|
||||
<div className={classnames(classes.row)}>
|
||||
<span className={classnames(classes.infoHeader)}>Scanner</span>
|
||||
<span className={classnames(classes.infoText)}>{scanner}</span>
|
||||
<div className="flex">
|
||||
<div className={classnames(classes.firstRow)}>
|
||||
<span className={classnames(classes.infoHeader)}>
|
||||
Thickness
|
||||
</span>
|
||||
<span className={classnames(classes.infoText)}>
|
||||
{thickness ? thickness : 'N/A'}
|
||||
</span>
|
||||
</div>
|
||||
<div className={classnames(classes.row)}>
|
||||
<span className={classnames(classes.infoHeader)}>
|
||||
Spacing
|
||||
</span>
|
||||
<span className={classnames(classes.infoText)}>
|
||||
{spacing}
|
||||
</span>
|
||||
</div>
|
||||
<div className={classnames(classes.row)}>
|
||||
<span className={classnames(classes.infoHeader)}>
|
||||
Scanner
|
||||
</span>
|
||||
<span className={classnames(classes.infoText)}>
|
||||
{scanner}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
)
|
||||
}
|
||||
>
|
||||
<div className="relative flex justify-end cursor-pointer">
|
||||
<div className="relative">
|
||||
|
||||
Reference in new issue
Block a user