OHIF-79: Study Browser side panel (#1614)

* Initial code changes for StudyBrowser

* StudyItem component basic strucure

* Adding new icon

* Updates to get StudyItem working

* Small fix on side panel open header

* Adding new icons

* Thumbnail List

* ThumbnailSR WIP

* Thumanil WIP

* Export new components into ohif/ui

* Updates on style

* Update ThumbnailSR style

* Margin updates

* MAke it scrollable

* Make it scrollable

* Style updates

* Fixing tooltip

* Tunning the mock data and testing scenarios

* Update mock data

* make all studies collapsed

* small fix on tooltip

* Removing empty mdx files

* Updating viewer docz page

* Style updates

* Update button font-size

* Fix: Make cancel button only visible on hover

* Fixing callouts

* Fix eslint
This commit is contained in:
Gustavo André Lelis 2020-04-13 07:59:58 -03:00 committed by James A. Petts
parent 51750c6ede
commit eda99a0bac
23 changed files with 569 additions and 123 deletions

View File

@ -30,6 +30,8 @@ export {
Select,
SegmentationTable,
SidePanel,
StudyBrowser,
StudyItem,
StudyListExpandedRow,
StudyListPagination,
StudyListTable,
@ -40,6 +42,9 @@ export {
TableHead,
TableRow,
ThemeWrapper,
Thumbnail,
ThumbnailSR,
ThumbnailList,
Typography,
} from './src/components';

View File

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<g fill="none" fill-rule="evenodd">
<circle cx="8" cy="8" r="8" fill="currentColor"/>
<path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11.556 5.333L7.824 11.11c-.168.259-.424.42-.703.444-.278.023-.552-.095-.75-.324L4.444 8.993"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 366 B

View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<path fill="none" fill-rule="evenodd" stroke="currentColor" stroke-dasharray="1.5 3.8" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M8 15c3.866 0 7-3.134 7-7s-3.134-7-7-7-7 3.134-7 7 3.134 7 7 7z"/>
</svg>

After

Width:  |  Height:  |  Size: 295 B

View File

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<g fill="none" fill-rule="evenodd">
<circle cx="8" cy="8" r="8" fill="currentColor"/>
<path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11.556 5.333L7.824 11.11c-.168.259-.424.42-.703.444-.278.023-.552-.095-.75-.324L4.444 8.993"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 366 B

View File

@ -16,25 +16,12 @@
@apply bg-primary-main;
}
/* INVISIBLE SCROLLBAR */
.invisible-scrollbar::-webkit-scrollbar {
@apply hidden;
}
.invisible-scrollbar::-webkit-scrollbar-track {
@apply hidden;
}
.invisible-scrollbar::-webkit-scrollbar-thumb {
@apply hidden;
}
.invisible-scrollbar::-webkit-scrollbar-thumb:window-inactive {
@apply hidden;
}
/* TOOLTIP WORKAROUND FOR ARROW UP */
.tooltip::before {
.tooltip.tooltip-up::before {
@apply bg-primary-dark absolute z-10;
content: '';
width: 14px;
@ -43,3 +30,34 @@
left: 50%;
transform: translateX(-50%);
}
.tooltip.tooltip-right {
left: calc(100% + 15px);
}
.tooltip.tooltip-right::before {
@apply bg-black absolute z-10;
content: '';
width: 2px;
height: 15px;
left: -1px;
top: 50%;
transform: translateY(-50%);
}
/* TOOLTIP WORKAROUND FOR ARROW */
.showTooltipOnHover .tooltip {
display: none;
}
.showTooltipOnHover:hover .tooltip {
display: block;
}
.showExcludeButtonOnHover .excludeButton {
display: none;
}
.showExcludeButtonOnHover:hover .excludeButton {
display: block;
}

View File

@ -4,6 +4,8 @@ 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 dottedCircle from './../../assets/icons/dotted-circle.svg';
import circledCheckmark from './../../assets/icons/circled-checkmark.svg';
import chevronDown from './../../assets/icons/chevron-down.svg';
import chevronLeft from './../../assets/icons/chevron-left.svg';
import chevronRight from './../../assets/icons/chevron-right.svg';
@ -24,6 +26,7 @@ import settings from './../../assets/icons/settings.svg';
import sorting from './../../assets/icons/sorting.svg';
import sortingActiveDown from './../../assets/icons/sorting-active-down.svg';
import sortingActiveUp from './../../assets/icons/sorting-active-up.svg';
import tracked from './../../assets/icons/tracked.svg';
/** Tools */
import toolZoom from './../../assets/icons/tool-zoom.svg';
@ -41,6 +44,8 @@ const ICONS = {
'arrow-down': arrowDown,
calendar: calendar,
cancel: cancel,
'dotted-circle': dottedCircle,
'circled-checkmark': circledCheckmark,
'chevron-down': chevronDown,
'chevron-left': chevronLeft,
'chevron-right': chevronRight,
@ -61,6 +66,7 @@ const ICONS = {
'sorting-active-down': sortingActiveDown,
'sorting-active-up': sortingActiveUp,
sorting: sorting,
tracked: tracked,
/** Tools */
'tool-zoom': toolZoom,

View File

@ -77,7 +77,7 @@ const SidePanel = ({
onClick={() => {
setIsOpen(false);
}}
className="flex flex-row items-center border-b-2 border-secondary-main h-12 relative"
className="flex flex-row items-center border-b-2 border-secondary-light px-3 h-12"
>
<Icon
name={openIconName[side]}

View File

@ -0,0 +1,87 @@
import React, { useState } from 'react';
import classnames from 'classnames';
import { ButtonGroup, Button, StudyItem, ThumbnailList } from '@ohif/ui';
import { studyWithSR, studySimple } from './mockData';
const studyGroupTypes = ['Primary', 'Recent', 'All'];
const studyGroup = {
Primary: [studySimple],
Recent: [studyWithSR, studySimple],
All: [studySimple, studyWithSR],
};
const buttonClasses = 'text-white text-base border-none bg-black';
const activeButtonClasses = 'bg-primary-main';
const StudyBrowser = () => {
const [studyGroupSelected, setStudyGroupSelected] = useState('Recent');
const [studyActive, setStudyActive] = useState(null);
return (
<React.Fragment>
<div className="flex flex-row items-center justify-center border-b-2 w-100 h-16 border-secondary-light p-4 bg-primary-dark">
<ButtonGroup
variant="outlined"
color="inherit"
className="border border-secondary-light rounded-md"
>
{studyGroupTypes.map((studyGroup) => {
const isActive = studyGroupSelected === studyGroup;
return (
<Button
key={studyGroup}
className={classnames(
buttonClasses,
isActive && activeButtonClasses
)}
size="small"
onClick={() => {
setStudyGroupSelected(studyGroup);
setStudyActive(null);
}}
>
{studyGroup}
</Button>
);
})}
</ButtonGroup>
</div>
<div className="flex flex-col flex-1 overflow-auto invisible-scrollbar">
{studyGroup[studyGroupSelected].map(
({
studyInstanceUid,
studyDate,
studyDescription,
instances,
modalities,
trackedSeries,
thumbnails,
}) => {
const isActive = studyActive === studyInstanceUid;
return (
<React.Fragment key={studyInstanceUid}>
<StudyItem
studyDate={studyDate}
studyDescription={studyDescription}
instances={instances}
modalities={modalities}
trackedSeries={trackedSeries}
isActive={isActive}
onClick={() => {
setStudyActive(isActive ? null : studyInstanceUid);
}}
/>
{isActive && thumbnails && (
<ThumbnailList thumbnails={thumbnails} />
)}
</React.Fragment>
);
}
)}
</div>
</React.Fragment>
);
};
export default StudyBrowser;

View File

@ -0,0 +1,26 @@
---
name: StudyBrowser
menu: General
route: components/studyBrowser
---
import { Playground, Props } from 'docz';
import { StudyBrowser } from '@ohif/ui';
# StudyBrowser
## Import
```javascript
import { StudyBrowser } from '@ohif/ui';
```
## Basic usage
<Playground>
<StudyBrowser />
</Playground>
## Properties
<Props of={StudyBrowser} />

View File

@ -0,0 +1,2 @@
import StudyBrowser from './StudyBrowser';
export default StudyBrowser;

View File

@ -0,0 +1,56 @@
const studyWithSR = {
studyInstanceUid: '1',
studyDate: '07-Sept-2010',
studyDescription: 'CHEST/ABD/PELVIS W/CONTRAST',
instances: 902,
modalities: 'CT,SR',
trackedSeries: 1,
thumbnails: [
{
displaySetInstanceUid: 'f69f6asdasd48c-223e-db7f-c4af-b8906641a66e',
seriesDescription: 'Multiple line image series description lorem sit',
seriesNumber: 1,
instanceNumber: 68,
viewportIdentificator: 'A',
isTracked: true,
},
{
displaySetInstanceUid: 'f69f648c-223e-db7f-c4asdasdaf-b8906641a66e',
seriesDescription: 'Multiple line image series description lorem sit',
seriesNumber: 1,
instanceNumber: 68,
viewportIdentificator: 'B',
},
{
displaySetInstanceUid: 'f69f648c-223e-dasdasdb7f-c4af-b8906641a66e',
seriesDescription: 'Multiple line image series description lorem sit',
seriesNumber: 1,
instanceNumber: 68,
},
{
displaySetInstanceUid: 'f69f648c-223e-db7f-c4afas-b8906asd641a66e',
seriesDescription: 'Multiple line description lorem ipsum dolor sit amet',
modality: 'SR',
seriesDate: '07-Sept-2010',
},
],
};
const studySimple = {
studyInstanceUid: '2',
studyDate: '07-Sept-2010',
studyDescription: 'CHEST/ABD/PELVIS W/CONTRAST',
instances: 902,
modalities: 'CT',
trackedSeries: 0,
thumbnails: [
{
displaySetInstanceUid: 'f69f648c-223e-db7f-c4af-b8906641a66e',
seriesDescription: 'Multiple line image series description lorem sit',
seriesNumber: 1,
instanceNumber: 68,
},
],
};
export { studyWithSR, studySimple };

View File

@ -0,0 +1,72 @@
import React from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { Icon } from '@ohif/ui';
const baseClasses =
'first:border-0 border-t border-secondary-light cursor-pointer';
const StudyItem = ({
studyDate,
studyDescription,
instances,
modalities,
trackedSeries,
isActive,
onClick,
}) => {
return (
<div
className={classnames(
isActive ? 'bg-secondary-dark' : 'bg-black hover:bg-secondary-main',
baseClasses
)}
onClick={onClick}
onKeyDown={onClick}
role="button"
tabIndex="0"
>
<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="flex flex-row items-center text-blue-300 text-base">
<Icon name="group-layers" className="text-blue-300 mx-2 w-4" />
{instances}
</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>
</div>
{!!trackedSeries && (
<div className="flex flex-2">
<div
className={classnames(
'flex flex-row bg-secondary-main text-base text-white py-1 pl-2 pr-4 mt-2 ',
isActive
? 'flex-1 border-t border-secondary-light justify-center'
: 'rounded-sm mx-4 mb-4'
)}
>
<Icon name="tracked" className="text-primary-light w-4 mr-2" />
{trackedSeries} Tracked Series
</div>
</div>
)}
</div>
);
};
StudyItem.propTypes = {
studyDate: PropTypes.any,
studyDescription: PropTypes.any,
modalities: PropTypes.any,
instances: PropTypes.any,
trackedSeries: PropTypes.any,
isActive: PropTypes.any,
onClick: PropTypes.func.isRequired,
};
export default StudyItem;

View File

@ -0,0 +1,2 @@
import StudyItem from './StudyItem';
export default StudyItem;

View File

@ -0,0 +1,117 @@
import React from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { Icon } from '@ohif/ui';
const Thumbnail = ({
seriesDescription,
seriesNumber,
instanceNumber,
viewportIdentificator,
isTracked,
isActive,
onClick,
}) => {
const trackedIcon = isTracked ? 'circled-checkmark' : 'dotted-circle';
return (
<div
className="flex flex-row flex-1 px-4 py-2 showExcludeButtonOnHover cursor-pointer"
onClick={onClick}
onKeyDown={onClick}
role="button"
tabIndex="0"
>
<div className="flex flex-col flex-2 px-2 items-center">
<div
className={classnames(
'flex flex-col items-center justify-start p-2 mb-2 relative cursor-pointer',
isTracked && 'rounded-sm hover:bg-gray-900 showTooltipOnHover'
)}
>
<Icon name={trackedIcon} className="text-primary-light mb-2 w-5" />
<div className="text-white text-xl leading-tight h-5">
{viewportIdentificator}
</div>
<div
className={classnames(
'tooltip tooltip-right absolute bg-black border border-secondary-main text-common-light text-base rounded py-2 px-4 top-0 w-max-content hidden'
)}
>
<div className="flex flex-row flex-1">
<div className="flex flex-col flex-1 pr-4">
<span>
Series is <span className="text-white">tracked</span>
</span>
{viewportIdentificator && (
<span>
in viewport
<span className="ml-1 text-white">
{viewportIdentificator}
</span>
</span>
)}
</div>
<div className="flex flex-2 items-center justify-center">
<Icon name="info-link" className="text-primary-active" />
</div>
</div>
<svg
className="absolute text-black stroke-secondary-main stroke-2"
style={{
top: 'calc(50% - 8px)',
left: -15,
transform: 'rotate(270deg)',
}}
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
>
<path fill="currentColor" d="M24 22h-24l12-20z" />
</svg>
</div>
</div>
{isTracked && (
<Icon name="cancel" className="text-primary-active excludeButton" />
)}
</div>
<div className="flex flex-col flex-1 px-4">
<div
className={classnames(
'flex flex-1 items-center justify-center rounded-md bg-black text-white mb-2',
isActive
? 'border-2 border-primary-light'
: 'border border-secondary-light hover:border-blue-300'
)}
style={{ minHeight: '120px' }}
>
{'Thumbnail img'}
</div>
<div className="flex flex-row flex-1 text-blue-300 text-base">
<div className="mr-4">
<span className="text-primary-main font-bold">{'S: '}</span>
{seriesNumber}
</div>
<div className="flex flex-row items-centerflex-1">
<Icon name="group-layers" className="w-3 mr-2" /> {instanceNumber}
</div>
</div>
<div className="text-white text-base">{seriesDescription}</div>
</div>
</div>
);
};
Thumbnail.propTypes = {
seriesDescription: PropTypes.string.isRequired,
seriesNumber: PropTypes.number.isRequired,
instanceNumber: PropTypes.number.isRequired,
isActive: PropTypes.bool.isRequired,
onClick: PropTypes.func.isRequired,
isTracked: PropTypes.bool,
viewportIdentificator: PropTypes.string,
};
export default Thumbnail;

View File

@ -0,0 +1,2 @@
import Thumbnail from './Thumbnail';
export default Thumbnail;

View File

@ -0,0 +1,73 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { Thumbnail, ThumbnailSR } from '@ohif/ui';
const ThumbnailList = ({ thumbnails }) => {
const [thumbnailActive, setThumbnailActive] = useState(null);
return (
<div className="bg-black py-3">
{thumbnails.map(
({
displaySetInstanceUid,
seriesDescription,
seriesNumber,
instanceNumber,
modality,
seriesDate,
viewportIdentificator,
isTracked,
}) => {
const isSR = modality && modality.toLowerCase() === 'sr';
const isActive = thumbnailActive === displaySetInstanceUid;
if (isSR) {
return (
<ThumbnailSR
key={displaySetInstanceUid}
modality={modality}
seriesDate={seriesDate}
seriesDescription={seriesDescription}
/>
);
} else {
return (
<Thumbnail
key={displaySetInstanceUid}
seriesDescription={seriesDescription}
seriesNumber={seriesNumber}
instanceNumber={instanceNumber}
viewportIdentificator={viewportIdentificator}
isTracked={isTracked}
isActive={isActive}
onClick={() => {
setThumbnailActive(
thumbnailActive === displaySetInstanceUid
? null
: displaySetInstanceUid
);
}}
/>
);
}
}
)}
</div>
);
};
ThumbnailList.propTypes = {
thumbnails: PropTypes.arrayOf(
PropTypes.shape({
displaySetInstanceUid: PropTypes.string,
seriesDescription: PropTypes.string,
seriesNumber: PropTypes.number,
instanceNumber: PropTypes.number,
modality: PropTypes.string,
seriesDate: PropTypes.string,
})
),
};
export default ThumbnailList;

View File

@ -0,0 +1,2 @@
import ThumbnailList from './ThumbnailList';
export default ThumbnailList;

View File

@ -0,0 +1,31 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Icon } from '@ohif/ui';
const ThumbnailSR = ({ seriesDescription, seriesDate }) => {
return (
<div className="flex flex-row flex-1 px-4 py-3">
<div className="flex flex-2 pl-4 pr-6">
<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">
SR
</div>
<span className="text-blue-300 text-base">{seriesDate}</span>
</div>
<div className="text-white text-base">{seriesDescription}</div>
</div>
<div></div>
</div>
);
};
ThumbnailSR.propTypes = {
seriesDescription: PropTypes.string,
seriesDate: PropTypes.string,
};
export default ThumbnailSR;

View File

@ -0,0 +1,2 @@
import ThumbnailSR from './ThumbnailSR';
export default ThumbnailSR;

View File

@ -16,16 +16,21 @@ import NavBar from './NavBar';
import Select from './Select';
import SegmentationTable from './SegmentationTable';
import SidePanel from './SidePanel';
import Svg from './Svg';
import StudyBrowser from './StudyBrowser';
import StudyListExpandedRow from './StudyListExpandedRow';
import StudyListPagination from './StudyListPagination';
import StudyListTable from './StudyListTable';
import Svg from './Svg';
import StudyItem from './StudyItem';
import Table from './Table';
import TableBody from './TableBody';
import TableCell from './TableCell';
import TableHead from './TableHead';
import TableRow from './TableRow';
import ThemeWrapper from './ThemeWrapper/';
import Thumbnail from './Thumbnail';
import ThumbnailSR from './ThumbnailSR';
import ThumbnailList from './ThumbnailList';
import Typography from './Typography';
export {
@ -47,15 +52,20 @@ export {
Select,
SegmentationTable,
SidePanel,
Svg,
StudyBrowser,
StudyItem,
StudyListExpandedRow,
StudyListPagination,
StudyListTable,
Svg,
Table,
TableBody,
TableCell,
TableHead,
TableRow,
ThemeWrapper,
Thumbnail,
ThumbnailSR,
ThumbnailList,
Typography,
};

View File

@ -86,7 +86,7 @@ components.
{shouldShowTooltip && (
<div
className={classnames(
'tooltip absolute bg-primary-dark border border-secondary-main text-white text-base rounded py-1 px-4 inset-x-auto top-full mt-2 w-max-content'
'tooltip tooltip-up absolute bg-primary-dark border border-secondary-main text-white text-base rounded py-1 px-4 inset-x-auto top-full mt-2 w-max-content'
)}
>
{tool.label}

View File

@ -1,5 +1,5 @@
import React from 'react';
import { NavBar, SidePanel, Svg } from '@ohif/ui';
import { NavBar, SidePanel, Svg, StudyBrowser } from '@ohif/ui';
const Viewer = () => {
return (
@ -18,11 +18,9 @@ const Viewer = () => {
iconName="group-layers"
iconLabel="Studies"
componentLabel="Studies"
defaultIsOpen={false}
defaultIsOpen={true}
>
<div className="flex justify-center text-white p-2">
panel placeholder
</div>
<StudyBrowser />
</SidePanel>
<div className="flex flex-1 h-100 overflow-hidden bg-primary-main items-center justify-center text-white">
CONTENT

View File

@ -5,110 +5,32 @@ route: examples/viewer
---
import { Playground, Props } from 'docz';
import {
NavBar,
SidePanel,
Svg,
MeasurementTable,
SegmentationTable,
ButtonGroup,
Button,
Icon,
IconButton,
} from '@ohif/ui';
import { NavBar, SidePanel, Svg, StudyBrowser } from '@ohif/ui';
# Viewer
<Playground>
{() => {
const measurementTableData = {
title: 'Measurements',
amount: 10,
data: new Array(10).fill(''),
};
const segmentationTableData = {
title: 'Segments',
amount: 12,
data: new Array(12).fill(''),
};
return (
<div>
<NavBar className="justify-start">
<div className="m-3">
<Svg name="logo-ohif" />
</div>
</NavBar>
<div
className="flex flex-row flex-no-wrap flex-1 items-stretch overflow-hidden w-full"
style={{ height: 'calc(100vh - 57px' }}
>
<SidePanel
side="left"
iconName="group-layers"
iconLabel="Studies"
componentLabel="Studies"
defaultIsOpen={false}
>
<div className="flex justify-center text-white p-2">
panel placeholder
</div>
</SidePanel>
<div className="flex flex-1 h-100 overflow-hidden bg-primary-main items-center justify-center text-white">
CONTENT
</div>
<SidePanel
side="right"
iconName="list-bullets"
iconLabel="Measure"
componentLabel="Measurements"
defaultIsOpen={false}
>
<div className="overflow-y-auto overflow-x-hidden invisible-scrollbar">
<div className="p-2">
<div className="leading-none">
<span className="text-white text-base mr-2">07-Sep-2010</span>
<span className="px-1 text-black bg-common-bright text-base rounded-sm font-bold">
CT
</span>
</div>
<div className="leading-none">
<span className="text-base text-primary-light">
CHEST/ABD/PELVIS W CONTRAST
</span>
</div>
</div>
<MeasurementTable {...measurementTableData} />
<SegmentationTable {...segmentationTableData} />
</div>
<div className="p-4 flex justify-center">
<ButtonGroup onClick={() => alert('Export')}>
<Button
className="text-white border-primary-main bg-black text-base py-2 px-2"
size="initial"
color="inherit"
>
Export
</Button>
<IconButton
className="bg-black border-primary-main px-2 text-white px-2"
color="inherit"
size="initial"
>
<Icon name="arrow-down" />
</IconButton>
</ButtonGroup>
<Button
className="text-white border border-primary-main bg-black text-base py-2 px-2 ml-2"
variant="outlined"
size="initial"
color="inherit"
onClick={() => alert('Create Report')}
>
Create Report
</Button>
</div>
</SidePanel>
</div>
<div>
<NavBar className="justify-start">
<div className="m-3">
<Svg name="logo-ohif" />
</div>
</NavBar>
<div
className="flex flex-row flex-no-wrap flex-1 items-stretch overflow-hidden w-full"
style={{ height: 'calc(100vh - 57px' }}
>
<SidePanel
side="left"
iconName="group-layers"
iconLabel="Studies"
componentLabel="Studies"
defaultIsOpen={false}
>
<StudyBrowser />
</SidePanel>
<div className="flex flex-1 h-100 overflow-hidden bg-primary-main items-center justify-center text-white">
CONTENT
</div>
);
}}