Merge branch 'feat/v2-main' of https://github.com/OHIF/Viewers into feat/ui-v2-Dropdown

This commit is contained in:
Rodrigo Antinarelli committed 2020-06-26 12:42:56 -03:00
commit 0d53346d61
73 files changed
+1423 -369

No files matched your search

+2
View File
@@ -33,6 +33,8 @@ export {
Dialog,
Dropdown,
EmptyStudies,
ExpandableToolbarButton,
ListMenu,
Icon,
IconButton,
Input,
+7
View File
@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" width="19" height="19" viewBox="0 0 19 19">
<g fill="currentColor" fill-rule="evenodd">
<g stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5">
<path d="M.188.187L8.813 8.812M8.813.187L.188 8.812" transform="translate(5 5)"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 355 B

@@ -3,12 +3,13 @@ import PropTypes from 'prop-types';
import classnames from 'classnames';
import { Icon, Typography } from '@ohif/ui';
const EmptyStudies = ({ className }) => {
// TODO: Add loading spinner to OHIF + use it here.
const EmptyStudies = ({ className, isLoading }) => {
return (
<div className={classnames('flex-col inline-flex items-center', className)}>
<Icon name="magnifier" className="mb-4" />
<Typography className="text-primary-light" variant="h5">
No studies available
{!isLoading ? 'No studies available' : 'Loading...'}
</Typography>
</div>
);
@@ -0,0 +1,16 @@
.ExpandableToolbarButton:hover .ExpandableToolbarButton__arrow:after {
content: "";
position: absolute;
bottom: -10px;
border-width: 10px 10px 0;
border-style: solid;
border-color:#5acce6 transparent;
}
.ExpandableToolbarButton .ExpandableToolbarButton__content {
display: none;
}
.ExpandableToolbarButton:hover .ExpandableToolbarButton__content {
display: block;
}
@@ -0,0 +1,86 @@
import React from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { IconButton, Icon } from '@ohif/ui';
import './ExpandableToolbarButton.css';
const ExpandableToolbarButton = ({
type,
id,
isActive,
onClick,
icon,
className,
content: Content,
contentProps
}) => {
const classes = {
type: {
primary: isActive
? 'text-black'
: 'text-common-bright hover:bg-primary-dark hover:text-primary-light',
secondary: isActive
? 'text-black'
: 'text-white hover:bg-secondary-dark focus:bg-secondary-dark',
},
};
const onChildClickHandler = (...args) => {
onClick(...args);
if (contentProps.onClick) {
contentProps.onClick(...args);
}
};
const onClickHandler = (...args) => {
onClick(...args);
};
return (
<div key={id} className="ExpandableToolbarButton">
<IconButton
variant={isActive ? 'contained' : 'text'}
className={classnames(
'mx-1',
classes.type[type],
isActive && 'ExpandableToolbarButton__arrow'
)}
onClick={onClickHandler}
key={id}
>
<Icon name={icon} />
</IconButton>
<div className="absolute z-10 pt-4">
<div className={classnames("ExpandableToolbarButton__content w-48", className)}>
<Content {...contentProps} onClick={onChildClickHandler} />
</div>
</div>
</div>
);
};
const noop = () => { };
ExpandableToolbarButton.defaultProps = {
isActive: false,
type: 'primary',
content: null,
onClick: noop,
};
ExpandableToolbarButton.propTypes = {
/* Influences background/hover styling */
type: PropTypes.oneOf(['primary', 'secondary']),
id: PropTypes.string.isRequired,
isActive: PropTypes.bool,
onClick: PropTypes.func.isRequired,
icon: PropTypes.string.isRequired,
/** Expandable toolbar button content can be replaced for a customized content by passing a node to this value. */
content: PropTypes.oneOfType([PropTypes.node, PropTypes.func]),
contentProps: PropTypes.object,
};
export default ExpandableToolbarButton;
@@ -0,0 +1,58 @@
---
name: Expandable Toolbar Button
menu: General
route: components/expandableToolbarButton
---
import { useState } from 'react';
import { Playground, Props } from 'docz';
import { ExpandableToolbarButton, ListMenu } from '@ohif/ui';
# Toolbar Button
Expandable Toolbar Buttons are used to populate the Toolbar.
## Import
```javascript
import { ExpandableToolbarButton, ListMenu } from '@ohif/ui';
```
<Playground>
{() => {
const props = {
content: ListMenu,
contentProps: {
options: [
{ value: 'windowLevelPreset1', title: 'Soft tissue', subtitle: '400 / 40' },
{ value: 'windowLevelPreset2', title: 'Lung', subtitle: '1500 / -600' },
{ value: 'windowLevelPreset3', title: 'Liver', subtitle: '150 / 90' },
{ value: 'windowLevelPreset4', title: 'Bone', subtitle: '80 / 40' },
{ value: 'windowLevelPreset5', title: 'Brain', subtitle: '2500 / 480' },
],
renderer: ({ title, subtitle, isActive, index }) => (
<>
<div>
<span className={classnames(isActive ? "text-black" : "text-white", "mr-2 text-base")}>
{title}
</span>
<span className={classnames(isActive ? "text-black" : "text-aqua-pale", "font-thin text-sm")}>
{subtitle}
</span>
</div>
<span className={classnames(isActive ? "text-black" : "text-primary-active", "text-sm")}>{index + 1}</span>
</>
)
}
};
return (
<div className="py-10 flex justify-center">
<ExpandableToolbarButton {...props} />
</div>
);
}}
</Playground>
## Properties
<Props of={ToolbarButton} />
@@ -0,0 +1,2 @@
import ExpandableToolbarButton from './ExpandableToolbarButton';
export default ExpandableToolbarButton;
@@ -4,6 +4,7 @@ import React from 'react';
import arrowDown from './../../assets/icons/arrow-down.svg';
import calendar from './../../assets/icons/calendar.svg';
import cancel from './../../assets/icons/cancel.svg';
import close from './../../assets/icons/close.svg';
import dottedCircle from './../../assets/icons/dotted-circle.svg';
import circledCheckmark from './../../assets/icons/circled-checkmark.svg';
import chevronDown from './../../assets/icons/chevron-down.svg';
@@ -49,6 +50,7 @@ const ICONS = {
'arrow-down': arrowDown,
calendar: calendar,
cancel: cancel,
close: close,
'dotted-circle': dottedCircle,
'circled-checkmark': circledCheckmark,
'chevron-down': chevronDown,
@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
import classnames from 'classnames';
const baseClasses =
'text-center items-center justify-center outline-none transition duration-300 ease-in-out font-bold focus:outline-none';
'text-center items-center justify-center transition duration-300 ease-in-out outline-none font-bold focus:outline-none';
const roundedClasses = {
none: '',
@@ -84,7 +84,7 @@ const IconButton = ({
}) => {
const buttonElement = useRef(null);
const handleOnClick = (e) => {
const handleOnClick = e => {
buttonElement.current.blur();
onClick(e);
};
+3 -3
View File
@@ -4,11 +4,11 @@ import Label from '../Label';
import classnames from 'classnames';
const baseInputClasses =
'shadow transition duration-300 appearance-none border rounded w-full py-2 px-3 text-sm text-white hover:border-gray-500 leading-tight focus:border-gray-500 focus:outline-none';
'shadow transition duration-300 appearance-none border border-primary-main hover:border-gray-500 focus:border-gray-500 focus:outline-none rounded w-full py-2 px-3 mt-2 text-sm text-white leading-tight focus:outline-none';
const transparentClasses = {
true: 'bg-transparent',
false: '',
false: 'bg-black',
};
const Input = ({
@@ -16,7 +16,7 @@ const Input = ({
containerClassName = '',
labelClassName = '',
className = '',
transparent = true,
transparent = false,
type = 'text',
value,
onChange,
@@ -43,7 +43,7 @@ InputText.propTypes = {
isSortable: PropTypes.bool,
sortDirection: PropTypes.oneOf(['ascending', 'descending', 'none']),
onLabelClick: PropTypes.func,
value: PropTypes.string,
value: PropTypes.any,
onChange: PropTypes.func.isRequired,
};
@@ -0,0 +1,65 @@
import React, { useState } from 'react';
import classnames from 'classnames';
import PropTypes from 'prop-types';
const ListMenu = ({ options = [], renderer, onClick }) => {
const [selectedIndex, setSelectedIndex] = useState(null);
const ListItem = (props) => {
const flex = 'flex flex-row justify-between items-center';
const theme = 'bg-indigo-dark';
const hover = 'hover:bg-primary-dark';
const spacing = 'p-3 h-8';
return (
<div
className={classnames(
flex,
theme,
spacing,
'cursor-pointer',
!props.isActive && hover,
props.isActive && 'bg-primary-light',
)}
onClick={props.onClick}
>
{renderer && renderer(props)}
</div>
);
};
return (
<div className="flex flex-col rounded-md bg-secondary-dark pt-2 pb-2">
{options.map((option, index) => {
const onClickHandler = () => {
setSelectedIndex(index);
onClick({ ...option, index });
};
return (
<ListItem
key={`ListItem${index}`}
{...option}
index={index}
isActive={selectedIndex === index}
onClick={onClickHandler}
/>
);
})}
</div>
);
};
const noop = () => { };
ListMenu.propTypes = {
options: PropTypes.array.isRequired,
renderer: PropTypes.func.isRequired,
onClick: PropTypes.func
};
ListMenu.defaultProps = {
onClick: noop
};
export default ListMenu;
@@ -0,0 +1,52 @@
---
name: List Menu
menu: General
route: components/listMenu
---
import { useState } from 'react';
import { Playground, Props } from 'docz';
import { ListMenu } from '@ohif/ui';
# List Menu
List Menus are used to populate expandable Toolbar.
## Import
```javascript
import { ListMenu } from '@ohif/ui';
```
<Playground>
{() => {
return (
<ToolbarButton options={[
{ value: 'windowLevelPreset1', title: 'Soft tissue', subtitle: '400 / 40' },
{ value: 'windowLevelPreset2', title: 'Lung', subtitle: '1500 / -600' },
{ value: 'windowLevelPreset3', title: 'Liver', subtitle: '150 / 90' },
{ value: 'windowLevelPreset4', title: 'Bone', subtitle: '80 / 40' },
{ value: 'windowLevelPreset5', title: 'Brain', subtitle: '2500 / 480' },
]}
renderer={
({ title, subtitle, isActive, index }) => (
<>
<div>
<span className={classnames(isActive ? "text-black" : "text-white", "mr-2 text-base")}>
{title}
</span>
<span className={classnames(isActive ? "text-black" : "text-aqua-pale", "font-thin text-sm")}>
{subtitle}
</span>
</div>
<span className={classnames(isActive ? "text-black" : "text-primary-active", "text-sm")}>{index + 1}</span>
</>
)}
/>
);
}}
</Playground>
## Properties
<Props of={ToolbarButton} />
@@ -0,0 +1,2 @@
import ListMenu from './ListMenu';
export default ListMenu;
@@ -14,7 +14,7 @@ const MeasurementTable = ({ data, title, amount, onClick, onEdit }) => {
</div>
<div className="overflow-y-auto overflow-x-hidden ohif-scrollbar max-h-112">
{!!data.length &&
data.map((measurementItem) => {
data.map(measurementItem => {
const { id, label, displayText, isActive } = measurementItem;
return (
<div
@@ -45,9 +45,11 @@ const MeasurementTable = ({ data, title, amount, onClick, onEdit }) => {
<span className="text-base text-primary-light mb-1">
{label}
</span>
<span className="pl-2 border-l border-primary-light text-base text-white">
{displayText}
</span>
{displayText.map(line => (
<span className="pl-2 border-l border-primary-light text-base text-white">
{line}
</span>
))}
<Icon
className={classnames(
'text-white w-4 absolute cursor-pointer transition duration-300',
@@ -61,7 +63,7 @@ const MeasurementTable = ({ data, title, amount, onClick, onEdit }) => {
right: 4,
transform: isActive ? '' : 'translateX(100%)',
}}
onClick={(e) => {
onClick={e => {
// stopPropagation needed to avoid disable the current active item
e.stopPropagation();
onEdit(id);
@@ -108,7 +110,7 @@ MeasurementTable.propTypes = {
PropTypes.shape({
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
label: PropTypes.string,
displayText: PropTypes.string,
displayText: PropTypes.arrayOf(PropTypes.string),
isActive: PropTypes.bool,
})
),
@@ -0,0 +1,3 @@
.modal-content {
max-height: calc(100vh - theme('spacing.250px'));
}
+14 -31
View File
@@ -1,25 +1,14 @@
import React from 'react';
import PropTypes from 'prop-types';
import ReactModal from 'react-modal';
import classNames from 'classnames';
import { Typography, useModal } from '@ohif/ui';
import './Modal.css';
const customStyle = {
overlay: {
zIndex: 1071,
backgroundColor: 'rgb(0, 0, 0, 0.8)',
display: 'flex',
alignItems: 'flex-start',
justifyContent: 'center',
padding: '40px 0',
},
};
import { Typography, useModal, IconButton, Icon } from '@ohif/ui';
ReactModal.setAppElement(document.getElementById('root'));
const Modal = ({
className,
closeButton,
shouldCloseOnEsc,
isOpen,
@@ -39,13 +28,15 @@ const Modal = ({
<header className="mb-6 pb-4 border-b border-secondary-main">
<Typography variant="h4">{title}</Typography>
{closeButton && (
<button
className="absolute top-0 right-0 bg-primary-main focus:outline-none text-white rounded-full text-2xl w-8 h-8 flex justify-center items-center -mr-3 -mt-3"
<IconButton
className="absolute top-0 right-0 focus:outline-none flex -mr-3 -mt-3"
data-cy="close-button"
color="primary"
onClick={onClose}
rounded="full"
>
×
</button>
<Icon name="close" className="text-white w-8 h-8" />
</IconButton>
)}
</header>
)
@@ -54,24 +45,17 @@ const Modal = ({
return (
<ReactModal
className={classNames(
`relative py-6 w-11/12 lg:w-10/12 xl:w-1/2 max-h-full outline-none bg-primary-dark border border-secondary-main text-white rounded ${className}`
)}
className="relative py-6 w-11/12 lg:w-10/12 xl:w-1/2 max-h-full outline-none bg-primary-dark border border-secondary-main text-white rounded"
overlayClassName="fixed top-0 left-0 right-0 bottom-0 z-50 bg-overlay flex items-start justify-center py-16"
shouldCloseOnEsc={shouldCloseOnEsc}
onRequestClose={handleClose}
isOpen={isOpen}
title={title}
style={customStyle}
>
<>
<div className="px-6">{renderHeader()}</div>
<section
className="ohif-scrollbar overflow-y-auto px-6"
style={{ maxHeight: 'calc(100vh - 200px)' }}
>
{children}
</section>
</>
<div className="px-6">{renderHeader()}</div>
<section className="ohif-scrollbar modal-content overflow-y-auto px-6">
{children}
</section>
</ReactModal>
);
};
@@ -81,7 +65,6 @@ Modal.defaultProps = {
};
Modal.propTypes = {
className: PropTypes.string,
closeButton: PropTypes.bool,
shouldCloseOnEsc: PropTypes.bool,
isOpen: PropTypes.bool,
+3 -9
View File
@@ -76,15 +76,9 @@ const Select = ({
options={options}
value={selectedOptions}
onChange={(selectedOptions, { action }) => {
if (!isMulti) {
onChange(selectedOptions, action);
return;
}
const newSelection = selectedOptions.reduce(
(acc, curr) => acc.concat([curr.value]),
[]
);
const newSelection = !selectedOptions.length
? selectedOptions
: selectedOptions.reduce((acc, curr) => acc.concat([curr.value]), []);
onChange(newSelection, action);
}}
></ReactSelect>
@@ -48,6 +48,7 @@ const StudyListFilter = ({
color="inherit"
className="text-primary-active"
startIcon={<Icon name="info-link" className="w-2" />}
onClick={showLearnMoreContent}
>
<span className="flex flex-col flex-1">
<span onClick={showLearnMoreContent}>Learn more</span>
@@ -25,6 +25,7 @@ const ToolbarButton = ({
};
const shouldShowDropdown = !!isActive && !!dropdownContent;
return (
<div key={id}>
<Tooltip
@@ -24,7 +24,7 @@ const arrowPositionStyle = {
},
};
const Tooltip = ({ content, isSticky, position, tight, children }) => {
const Tooltip = ({ content, isSticky, position, tight, children, isDisabled }) => {
const [isActive, setIsActive] = useState(false);
const handleMouseOver = () => {
@@ -39,7 +39,7 @@ const Tooltip = ({ content, isSticky, position, tight, children }) => {
}
};
const isOpen = isSticky || isActive;
const isOpen = (isSticky || isActive) && !isDisabled;
return (
<div
@@ -84,10 +84,12 @@ Tooltip.defaultProps = {
tight: false,
isSticky: false,
position: 'bottom',
isDisabled: false
};
Tooltip.propTypes = {
// Allow null
/** prevents tooltip from rendering despite hover/active/sticky */
isDisabled: PropTypes.bool,
content: PropTypes.oneOfType([PropTypes.node, PropTypes.func]),
position: PropTypes.oneOf([
'bottom',
@@ -1,4 +1,4 @@
import React from 'react';
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { Icon, ButtonGroup, Button, Tooltip } from '@ohif/ui';
@@ -13,9 +13,11 @@ const classes = {
const ViewportActionBar = ({
studyData,
showNavArrows,
showPatientInfo,
showPatientInfo: patientInfoVisibility,
onSeriesChange,
}) => {
const [showPatientInfo, setShowPatientInfo] = useState(patientInfoVisibility);
const {
label,
isTracked,
@@ -37,6 +39,8 @@ const ViewportActionBar = ({
scanner,
} = patientInformation;
const onPatientInfoClick = () => setShowPatientInfo(!showPatientInfo)
const renderIconStatus = () => {
if (modality === 'SR') {
return (
@@ -60,29 +64,30 @@ const ViewportActionBar = ({
{!isTracked ? (
<Icon name="dotted-circle" className="w-6 text-primary-light" />
) : (
<Tooltip
position="bottom-left"
content={
<div className="flex py-2">
<div className="flex pt-1">
<Icon name="info-link" className="w-4 text-primary-main" />
</div>
<div className="flex ml-4">
<span className="text-base text-common-light">
Series is
<Tooltip
position="bottom-left"
content={
<div className="flex py-2">
<div className="flex pt-1">
<Icon name="info-link" className="w-4 text-primary-main" />
</div>
<div className="flex ml-4">
<span className="text-base text-common-light">
Series is
<span className="font-bold text-white"> tracked</span> and
can be viewed <br /> in the measurement panel
</span>
</div>
</div>
</div>
}
>
<Icon name="tracked" className="w-6 text-primary-light" />
</Tooltip>
)}
}
>
<Icon name="tracked" className="w-6 text-primary-light" />
</Tooltip>
)}
</div>
);
};
return (
<div className="flex items-center p-2 border-b border-primary-light">
<div className="flex flex-grow">
@@ -131,19 +136,18 @@ const ViewportActionBar = ({
</ButtonGroup>
</div>
)}
{showPatientInfo && (
<div className="flex ml-4 mr-2">
<PatientInfo
patientName={patientName}
patientSex={patientSex}
patientAge={patientAge}
MRN={MRN}
thickness={thickness}
spacing={spacing}
scanner={scanner}
/>
</div>
)}
<div className="flex ml-4 mr-2 cursor-pointer" onClick={onPatientInfoClick}>
<PatientInfo
isOpen={showPatientInfo}
patientName={patientName}
patientSex={patientSex}
patientAge={patientAge}
MRN={MRN}
thickness={thickness}
spacing={spacing}
scanner={scanner}
/>
</div>
</div>
);
};
@@ -174,7 +178,7 @@ ViewportActionBar.propTypes = {
ViewportActionBar.defaultProps = {
showNavArrows: true,
showPatientInfo: true,
showPatientInfo: false,
};
function PatientInfo({
@@ -185,11 +189,14 @@ function PatientInfo({
thickness,
spacing,
scanner,
isOpen,
}) {
return (
<Tooltip
isSticky
isDisabled={!isOpen}
position="bottom-right"
content={
content={isOpen && (
<div className="flex py-2">
<div className="flex pt-1">
<Icon name="info-link" className="w-4 text-primary-main" />
@@ -236,7 +243,7 @@ function PatientInfo({
</div>
</div>
</div>
}
)}
>
<div className="relative flex justify-end">
<div className="relative">
@@ -6,10 +6,11 @@ import React, {
useRef,
} from 'react';
import classnames from 'classnames';
import {
Typography,
InputText,
InputNumber,
Input,
Tooltip,
IconButton,
Icon,
@@ -169,7 +170,10 @@ const ViewportDownloadForm = ({
);
};
const validSize = value => (value >= minimumSize ? value : minimumSize);
const validSize = useCallback(
value => (value >= minimumSize ? value : minimumSize),
[minimumSize]
);
const loadAndUpdateViewports = useCallback(async () => {
const { width: scaledWidth, height: scaledHeight } = await loadImage(
@@ -274,7 +278,7 @@ const ViewportDownloadForm = ({
<div className="mt-6 flex flex-col">
<div className="w-full mb-4">
<InputText
<Input
data-cy="file-name"
value={filename}
onChange={value => setFilename(value)}
@@ -286,7 +290,8 @@ const ViewportDownloadForm = ({
<div className="flex w-1/3">
<div className="flex flex-col flex-grow">
<div className="w-full">
<InputNumber
<Input
type="number"
min={minimumSize}
max={maximumSize}
label="Image width (px)"
@@ -297,7 +302,8 @@ const ViewportDownloadForm = ({
{renderErrorHandler('width')}
</div>
<div className="w-full mt-4">
<InputNumber
<Input
type="number"
min={minimumSize}
max={maximumSize}
label="Image height (px)"
@@ -366,20 +372,18 @@ const ViewportDownloadForm = ({
<div className="mt-8">
<div
className="hidden"
style={{
height: viewportElementDimensions.height,
width: viewportElementDimensions.width,
position: 'absolute',
left: '9999px',
}}
ref={ref => setViewportElement(ref)}
>
<canvas
className={canvasClass}
className={classnames('block', canvasClass)}
style={{
height: downloadCanvas.height,
width: downloadCanvas.width,
display: 'block',
}}
width={downloadCanvas.width}
height={downloadCanvas.height}
+4
View File
@@ -41,6 +41,8 @@ import ThumbnailNoImage from './ThumbnailNoImage';
import ThumbnailTracked from './ThumbnailTracked';
import ThumbnailList from './ThumbnailList';
import ToolbarButton from './ToolbarButton';
import ExpandableToolbarButton from './ExpandableToolbarButton';
import ListMenu from './ListMenu';
import Tooltip from './Tooltip';
import Typography from './Typography';
import Viewport from './Viewport';
@@ -56,6 +58,8 @@ export {
Dialog,
Dropdown,
EmptyStudies,
ExpandableToolbarButton,
ListMenu,
Icon,
IconButton,
Input,
+8
View File
@@ -10,12 +10,20 @@ module.exports = {
xl: '1280px',
},
colors: {
overlay: 'rgba(0, 0, 0, 0.8)',
transparent: 'transparent',
black: '#000',
white: '#fff',
initial: 'initial',
inherit: 'inherit',
indigo: {
dark: '#0b1a42',
},
aqua: {
pale: '#7bb2ce',
},
primary: {
light: '#5acce6',
main: '#0944b3',