feat: OHIF-120 - Study List Route Template (#1693)

* fix pagination size

* clean up, start a very basic route configuration

* quick navigation between routes

* configure webpack to read .env variables

* export studylist filter

* split studylist filter into a component

* minor fix in studylist pagination

* update studylistfilter imports

* create studylist container with study list content

* moving file

* study list filter doc page

* catch change in ui-v2

* fix: 1px gap that shows text underneath

* Shift to routes folder

Co-authored-by: Danny Brown <danny.ri.brown@gmail.com>
This commit is contained in:
Rodrigo Antinarelli 2020-04-29 15:44:19 -03:00 committed by James A. Petts
parent 01b314522d
commit cee09fcf8d
11 changed files with 460 additions and 30 deletions

View File

@ -35,6 +35,7 @@ export {
StudyBrowser, StudyBrowser,
StudyItem, StudyItem,
StudyListExpandedRow, StudyListExpandedRow,
StudyListFilter,
StudyListPagination, StudyListPagination,
StudyListTable, StudyListTable,
Svg, Svg,

View File

@ -73,8 +73,11 @@ const StudyListFilter = ({
</div> </div>
</div> </div>
</div> </div>
<div className="sticky z-10 border-b-4 border-black" style={{ top: 58 }}> <div
<div className="bg-custom-navyDark pt-3 pb-3 "> className="sticky z-10 border-b-4 border-black"
style={{ top: '57px' }}
>
<div className="bg-primary-dark pt-3 pb-3 ">
<InputGroup <InputGroup
inputMeta={filtersMeta} inputMeta={filtersMeta}
values={filterValues} values={filterValues}

View File

@ -0,0 +1,118 @@
---
name: Study List Filter
menu: Data Display
route: components/studyListFilter
---
import { useState } from 'react';
import { Playground, Props } from 'docz';
import { StudyListFilter } from '@ohif/ui';
# Study List Filter
## Import
```javascript
import { StudyListFilter } from '@ohif/ui';
```
## Basic usage
<Playground>
{() => {
const defaultFilterValues = {
patientName: '',
mrn: '',
studyDate: {
startDate: null,
endDate: null,
},
description: '',
modality: undefined,
accession: '',
sortBy: '',
sortDirection: 'none',
page: 0,
resultsPerPage: 25,
};
const [filterValues, setFilterValues] = useState(defaultFilterValues);
const filtersMeta = [
{
name: 'patientName',
displayName: 'Patient Name',
inputType: 'Text',
isSortable: true,
gridCol: 4,
},
{
name: 'mrn',
displayName: 'MRN',
inputType: 'Text',
isSortable: true,
gridCol: 2,
},
{
name: 'studyDate',
displayName: 'Study date',
inputType: 'DateRange',
isSortable: true,
gridCol: 5,
},
{
name: 'description',
displayName: 'Description',
inputType: 'Text',
isSortable: true,
gridCol: 4,
},
{
name: 'modality',
displayName: 'Modality',
inputType: 'MultiSelect',
inputProps: {
options: [
{ value: 'SEG', label: 'SEG' },
{ value: 'CT', label: 'CT' },
{ value: 'MR', label: 'MR' },
{ value: 'SR', label: 'SR' },
],
},
isSortable: true,
gridCol: 3,
},
{
name: 'accession',
displayName: 'Accession',
inputType: 'Text',
isSortable: true,
gridCol: 4,
},
{
name: 'instances',
displayName: 'Instances',
inputType: 'None',
isSortable: true,
gridCol: 2,
},
];
const isFiltering = (filterValues, defaultFilterValues) => {
return Object.keys(defaultFilterValues).some(name => {
return filterValues[name] !== defaultFilterValues[name];
});
};
return (
<StudyListFilter
numOfStudies={100}
filtersMeta={filtersMeta}
filterValues={filterValues}
setFilterValues={setFilterValues}
clearFilters={() => setFilterValues(defaultFilterValues)}
isFiltering={isFiltering(filterValues, defaultFilterValues)}
/>
);
}}
</Playground>
## Properties
<Props of={StudyListFilter} />

View File

@ -0,0 +1 @@
export { default } from './StudyListFilter';

View File

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Button, ButtonGroup, Icon, IconButton, Typography } from '@ohif/ui'; import { Button, ButtonGroup, Icon, Typography } from '@ohif/ui';
const StudyListPagination = ({ const StudyListPagination = ({
onChangePage, onChangePage,
@ -21,8 +21,7 @@ const StudyListPagination = ({
<div className="relative mr-3"> <div className="relative mr-3">
<select <select
defaultValue={perPage} defaultValue={perPage}
className="block appearance-none w-full bg-transparent border border-custom-darkSlateBlue text-white text-base px-2 pr-4 rounded leading-tight focus:outline-none" className="block appearance-none w-full bg-transparent border border-common-active text-white text-base px-2 pr-4 rounded leading-tight focus:outline-none h-8"
style={{ height: 28 }}
onChange={e => onChangePerPage(e.target.value)} onChange={e => onChangePerPage(e.target.value)}
onBlur={() => {}} onBlur={() => {}}
> >
@ -48,22 +47,22 @@ const StudyListPagination = ({
Page {currentPage} Page {currentPage}
</Typography> </Typography>
<ButtonGroup color="primary"> <ButtonGroup color="primary">
<IconButton <Button
size="inherit" size="initial"
className="border-common-active px-4 py-2 text-base" className="border-common-active px-4 py-2 text-base"
color="white" color="white"
onClick={() => navigateToPage(1)} onClick={() => navigateToPage(1)}
> >
<>{`<<`}</> {`<<`}
</IconButton> </Button>
<Button <Button
size="inherit" size="initial"
className="border-common-active py-2 px-2 text-base" className="border-common-active py-2 px-2 text-base"
color="white" color="white"
onClick={() => navigateToPage(currentPage - 1)} onClick={() => navigateToPage(currentPage - 1)}
>{`< Previous`}</Button> >{`< Previous`}</Button>
<Button <Button
size="inherit" size="initial"
className="border-common-active py-2 px-4 text-base" className="border-common-active py-2 px-4 text-base"
color="white" color="white"
onClick={() => navigateToPage(currentPage + 1)} onClick={() => navigateToPage(currentPage + 1)}

View File

@ -24,6 +24,7 @@ import StudyListPagination from './StudyListPagination';
import StudyListTable from './StudyListTable'; import StudyListTable from './StudyListTable';
import Svg from './Svg'; import Svg from './Svg';
import StudyItem from './StudyItem'; import StudyItem from './StudyItem';
import StudyListFilter from './StudyListFilter';
import Table from './Table'; import Table from './Table';
import TableBody from './TableBody'; import TableBody from './TableBody';
import TableCell from './TableCell'; import TableCell from './TableCell';
@ -66,6 +67,7 @@ export {
StudyBrowser, StudyBrowser,
StudyItem, StudyItem,
StudyListExpandedRow, StudyListExpandedRow,
StudyListFilter,
StudyListPagination, StudyListPagination,
StudyListTable, StudyListTable,
Svg, Svg,

View File

@ -13,11 +13,11 @@ import {
Button, Button,
StudyListPagination, StudyListPagination,
StudyListTable, StudyListTable,
StudyListFilter,
} from '@ohif/ui'; } from '@ohif/ui';
// fix imports after refactor // fix imports after refactor
import Header from './components/Header'; import Header from './components/Header';
import StudyListFilter from './components/StudyListFilter';
const filtersMeta = [ const filtersMeta = [
{ {
@ -96,7 +96,7 @@ const defaultFilterValues = {
}; };
const isFiltering = (filterValues, defaultFilterValues) => { const isFiltering = (filterValues, defaultFilterValues) => {
return Object.keys(defaultFilterValues).some((name) => { return Object.keys(defaultFilterValues).some(name => {
return filterValues[name] !== defaultFilterValues[name]; return filterValues[name] !== defaultFilterValues[name];
}); });
}; };
@ -109,7 +109,7 @@ const StudyList = () => {
const tableDataSource = studies.map((study, key) => { const tableDataSource = studies.map((study, key) => {
const rowKey = key + 1; const rowKey = key + 1;
const isExpanded = expandedRows.some((k) => k === rowKey); const isExpanded = expandedRows.some(k => k === rowKey);
const { const {
AccessionNumber, AccessionNumber,
Modalities, Modalities,
@ -128,7 +128,7 @@ const StudyList = () => {
Instances: 'Instances', Instances: 'Instances',
}; };
const seriesTableDataSource = series.map((seriesItem) => { const seriesTableDataSource = series.map(seriesItem => {
const { SeriesNumber, Modality, instances } = seriesItem; const { SeriesNumber, Modality, instances } = seriesItem;
return { return {
description: 'Patient Protocol', description: 'Patient Protocol',
@ -238,8 +238,8 @@ const StudyList = () => {
</StudyListExpandedRow> </StudyListExpandedRow>
), ),
onClickRow: () => onClickRow: () =>
setExpandedRows((s) => setExpandedRows(s =>
isExpanded ? s.filter((n) => rowKey !== n) : [...s, rowKey] isExpanded ? s.filter(n => rowKey !== n) : [...s, rowKey]
), ),
isExpanded, isExpanded,
}; };
@ -248,13 +248,13 @@ const StudyList = () => {
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const [perPage, setPerPage] = useState(25); const [perPage, setPerPage] = useState(25);
const totalPages = Math.floor(numOfStudies / perPage); const totalPages = Math.floor(numOfStudies / perPage);
const onChangePage = (page) => { const onChangePage = page => {
if (page > totalPages) { if (page > totalPages) {
return; return;
} }
setCurrentPage(page); setCurrentPage(page);
}; };
const onChangePerPage = (perPage) => { const onChangePerPage = perPage => {
setPerPage(perPage); setPerPage(perPage);
setCurrentPage(1); setCurrentPage(1);
}; };

View File

@ -18,12 +18,10 @@ import {
EmptyStudies, EmptyStudies,
StudyListTable, StudyListTable,
StudyListPagination, StudyListPagination,
StudyListFilter
} from '@ohif/ui'; } from '@ohif/ui';
<!-- TEMPORARY IMPORTING -- SHOULD USE InputGroup after PR #1580 gets merged -->
import StudyListFilter from './components/StudyListFilter';
import { getMockedStudies } from '../../utils/';
import classnames from 'classnames'; import classnames from 'classnames';
import moment from 'moment'; import moment from 'moment';
@ -259,7 +257,7 @@ components.
}; };
}); });
const handleStudyList = (number) => { const handleStudyList = (number) => {
const studies = getMockedStudies(number); const studies = utils.getMockedStudies(number);
setStudies(studies); setStudies(studies);
setCurrentPage(1); setCurrentPage(1);
}; };

View File

@ -94,7 +94,7 @@ module.exports = (env, argv) => {
swDest: 'sw.js', swDest: 'sw.js',
swSrc: path.join(SRC_DIR, 'service-worker.js'), swSrc: path.join(SRC_DIR, 'service-worker.js'),
// Increase the limit to 4mb: // Increase the limit to 4mb:
// maximumFileSizeToCacheInBytes: 4 * 1024 * 1024 maximumFileSizeToCacheInBytes: 4 * 1024 * 1024,
}), }),
], ],
// https://webpack.js.org/configuration/dev-server/ // https://webpack.js.org/configuration/dev-server/

View File

@ -1,13 +1,320 @@
import React from 'react'; import React, { useState } from 'react';
import classnames from 'classnames';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import moment from 'moment';
import {
utils,
Icon,
StudyListExpandedRow,
Button,
NavBar,
Svg,
IconButton,
EmptyStudies,
StudyListTable,
StudyListPagination,
StudyListFilter,
} from '@ohif/ui';
function StudyListContainer() {
const defaultFilterValues = {
patientName: '',
mrn: '',
studyDate: {
startDate: null,
endDate: null,
},
description: '',
modality: undefined,
accession: '',
sortBy: '',
sortDirection: 'none',
page: 0,
resultsPerPage: 25,
};
const [filterValues, setFilterValues] = useState(defaultFilterValues);
const studies = utils.getMockedStudies(100);
const numOfStudies = studies.length;
const [expandedRows, setExpandedRows] = useState([]);
const filtersMeta = [
{
name: 'patientName',
displayName: 'Patient Name',
inputType: 'Text',
isSortable: true,
gridCol: 4,
},
{
name: 'mrn',
displayName: 'MRN',
inputType: 'Text',
isSortable: true,
gridCol: 2,
},
{
name: 'studyDate',
displayName: 'Study date',
inputType: 'DateRange',
isSortable: true,
gridCol: 5,
},
{
name: 'description',
displayName: 'Description',
inputType: 'Text',
isSortable: true,
gridCol: 4,
},
{
name: 'modality',
displayName: 'Modality',
inputType: 'MultiSelect',
inputProps: {
options: [
{ value: 'SEG', label: 'SEG' },
{ value: 'CT', label: 'CT' },
{ value: 'MR', label: 'MR' },
{ value: 'SR', label: 'SR' },
],
},
isSortable: true,
gridCol: 3,
},
{
name: 'accession',
displayName: 'Accession',
inputType: 'Text',
isSortable: true,
gridCol: 4,
},
{
name: 'instances',
displayName: 'Instances',
inputType: 'None',
isSortable: true,
gridCol: 2,
},
];
const isFiltering = (filterValues, defaultFilterValues) => {
return Object.keys(defaultFilterValues).some(name => {
return filterValues[name] !== defaultFilterValues[name];
});
};
const tableDataSource = studies.map((study, key) => {
const rowKey = key + 1;
const isExpanded = expandedRows.some(k => k === rowKey);
const {
AccessionNumber,
Modalities,
Instances,
StudyDescription,
PatientId,
PatientName,
StudyDate,
series,
} = study;
const seriesTableColumns = {
description: 'Description',
seriesNumber: 'Series',
modality: 'Modality',
Instances: 'Instances',
};
const seriesTableDataSource = series.map(seriesItem => {
const { SeriesNumber, Modality, instances } = seriesItem;
return {
description: 'Patient Protocol',
seriesNumber: SeriesNumber,
modality: Modality,
Instances: instances.length,
};
});
return {
row: [
{
key: 'patientName',
content: (
<>
<Icon
name={isExpanded ? 'chevron-down' : 'chevron-right'}
className="mr-4"
/>
{PatientName}
</>
),
gridCol: 4,
},
{
key: 'mrn',
content: PatientId,
gridCol: 2,
},
{
key: 'studyDate',
content: (
<div>
<span className="mr-4">
{moment(StudyDate).format('MMM-DD-YYYY')}
</span>
<span>{moment(StudyDate).format('hh:mm A')}</span>
</div>
),
gridCol: 5,
},
{
key: 'description',
content: StudyDescription,
gridCol: 4,
},
{
key: 'modality',
content: Modalities,
gridCol: 3,
},
{
key: 'accession',
content: AccessionNumber,
gridCol: 4,
},
{
key: 'instances',
content: (
<>
<Icon
name="group-layers"
className={classnames('inline-flex mr-2 w-4', {
'text-primary-active': isExpanded,
'text-secondary-light': !isExpanded,
})}
/>
{Instances}
</>
),
gridCol: 4,
},
],
expandedContent: (
<StudyListExpandedRow
seriesTableColumns={seriesTableColumns}
seriesTableDataSource={seriesTableDataSource}
>
<Button
rounded="full"
variant="contained"
className="mr-4 font-bold"
endIcon={<Icon name="launch-arrow" style={{ color: '#21a7c6' }} />}
>
<Link to="/viewer/123">Basic Viewer</Link>
</Button>
<Button
rounded="full"
variant="contained"
className="mr-4 font-bold"
endIcon={<Icon name="launch-arrow" style={{ color: '#21a7c6' }} />}
>
<Link to="/viewer/123">Segmentation</Link>
</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>
</StudyListExpandedRow>
),
onClickRow: () =>
setExpandedRows(s =>
isExpanded ? s.filter(n => rowKey !== n) : [...s, rowKey]
),
isExpanded,
};
});
const [currentPage, setCurrentPage] = useState(1);
const [perPage, setPerPage] = useState(25);
const totalPages = Math.floor(numOfStudies / perPage);
const onChangePage = page => {
if (page > totalPages) {
return;
}
setCurrentPage(page);
};
const onChangePerPage = perPage => {
setPerPage(perPage);
setCurrentPage(1);
};
const hasStudies = numOfStudies > 0;
const StudyListContainer = () => {
return ( return (
<div> <div
<div>StudyListContainer</div> className={classnames('bg-black h-full', {
<Link to="/viewer/123">Go to Viewer Route</Link> 'h-screen': !hasStudies,
})}
>
<NavBar className="justify-between border-b-4 border-black" isSticky>
<div className="flex items-center">
<div className="mx-3">
<Svg name="logo-ohif" />
</div>
</div>
<div className="flex items-center">
<span className="mr-3 text-common-light text-lg">
FOR INVESTIGATIONAL USE ONLY
</span>
<IconButton
variant="text"
color="inherit"
className="text-primary-active"
onClick={() => {}}
>
<React.Fragment>
<Icon name="settings" />
<Icon name="chevron-down" />
</React.Fragment>
</IconButton>
</div>
</NavBar>
<StudyListFilter
numOfStudies={numOfStudies}
filtersMeta={filtersMeta}
filterValues={filterValues}
setFilterValues={setFilterValues}
clearFilters={() => setFilterValues(defaultFilterValues)}
isFiltering={isFiltering(filterValues, defaultFilterValues)}
/>
{hasStudies ? (
<>
<StudyListTable
tableDataSource={tableDataSource.slice(
(currentPage - 1) * perPage,
(currentPage - 1) * perPage + perPage
)}
numOfStudies={numOfStudies}
filtersMeta={filtersMeta}
/>
<StudyListPagination
onChangePage={onChangePage}
onChangePerPage={onChangePerPage}
currentPage={currentPage}
perPage={perPage}
/>
</>
) : (
<div className="flex flex-col items-center justify-center pt-48">
<EmptyStudies />
</div>
)}
</div> </div>
); );
}; }
export default StudyListContainer; export default StudyListContainer;

View File

@ -1,6 +1,7 @@
import React from 'react'; import React from 'react';
import { Switch, Route } from 'react-router-dom'; import { Switch, Route } from 'react-router-dom';
// Route Components
import StudyListContainer from './StudyListContainer'; import StudyListContainer from './StudyListContainer';
import NotFound from './NotFound'; import NotFound from './NotFound';