feat: StudyListTable, ExpandedRow, API definitions (#1579)
* feat: StudyListTable Refactor * feat: StudyListTable + Props Refactor * fix expanded rows handler * update and fix study list doc page * fix date format * use moment to format date * expose new components * fix study list expanded row * Export and Create doc page for Pagination * API Definition, Export and Doc page for StudyListTable * StudyListTableRow * use real example to mount study list * move files * temporary change study list (viewer) structure * fix studyList * remove unused files * remove file * remove unused package * PR fixes * Remove console.dir Co-authored-by: Gustavo Lelis <galelis@gmail.com>
This commit is contained in:
parent
056f56f5b4
commit
f4b078912a
@ -27,6 +27,9 @@ export {
|
||||
Label,
|
||||
NavBar,
|
||||
Select,
|
||||
StudyListExpandedRow,
|
||||
StudyListPagination,
|
||||
StudyListTable,
|
||||
Svg,
|
||||
Table,
|
||||
TableBody,
|
||||
|
||||
@ -26,7 +26,6 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"classnames": "^2.2.6",
|
||||
"date-fns": "^2.10.0",
|
||||
"docz": "^2.2.0",
|
||||
"gatsby": "2.19.24",
|
||||
"gatsby-plugin-postcss": "^2.1.20",
|
||||
|
||||
@ -0,0 +1,50 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { Table, TableHead, TableBody, TableRow, TableCell } from '@ohif/ui';
|
||||
|
||||
const StudyListExpandedRow = ({
|
||||
seriesTableColumns,
|
||||
seriesTableDataSource,
|
||||
children,
|
||||
}) => {
|
||||
return (
|
||||
<div className="w-full bg-black py-4 pl-12 pr-2">
|
||||
<div className="block">{children}</div>
|
||||
<div className="mt-4">
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
{Object.keys(seriesTableColumns).map((columnKey) => {
|
||||
return (
|
||||
<TableCell key={columnKey}>
|
||||
{seriesTableColumns[columnKey]}
|
||||
</TableCell>
|
||||
);
|
||||
})}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
|
||||
<TableBody>
|
||||
{seriesTableDataSource.map((row, i) => (
|
||||
<TableRow key={i}>
|
||||
{Object.keys(row).map((cellKey) => {
|
||||
const content = row[cellKey];
|
||||
return <TableCell key={cellKey}>{content}</TableCell>;
|
||||
})}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
StudyListExpandedRow.propTypes = {
|
||||
seriesTableDataSource: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
seriesTableColumns: PropTypes.object.isRequired,
|
||||
children: PropTypes.node.isRequired,
|
||||
};
|
||||
|
||||
export default StudyListExpandedRow;
|
||||
2
platform/ui/src/components/StudyListExpandedRow/index.js
Normal file
2
platform/ui/src/components/StudyListExpandedRow/index.js
Normal file
@ -0,0 +1,2 @@
|
||||
import StudyListExpandedRow from './StudyListExpandedRow';
|
||||
export default StudyListExpandedRow;
|
||||
@ -1,18 +1,22 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Typography,
|
||||
ButtonGroup,
|
||||
Button,
|
||||
IconButton,
|
||||
Icon,
|
||||
} from '../../../components/';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Button, ButtonGroup, Icon, IconButton, Typography } from '@ohif/ui';
|
||||
|
||||
<<<<<<< HEAD:platform/ui/src/views/StudyList/components/StudyListPagination.js
|
||||
const StudyListPagination = (props) => {
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
|
||||
=======
|
||||
const StudyListPagination = ({
|
||||
onChangePage,
|
||||
currentPage,
|
||||
perPage,
|
||||
onChangePerPage,
|
||||
}) => {
|
||||
>>>>>>> d5a288db1... feat: StudyListTable, ExpandedRow, API definitions (#1579):platform/ui/src/components/StudyListPagination/StudyListPagination.js
|
||||
const navigateToPage = (page) => {
|
||||
const toPage = page < 1 ? 1 : page;
|
||||
setCurrentPage(toPage);
|
||||
onChangePage(toPage);
|
||||
};
|
||||
|
||||
return (
|
||||
@ -22,9 +26,15 @@ const StudyListPagination = (props) => {
|
||||
<div className="flex items-center">
|
||||
<div className="relative mr-3">
|
||||
<select
|
||||
<<<<<<< HEAD:platform/ui/src/views/StudyList/components/StudyListPagination.js
|
||||
defaultValue="25"
|
||||
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"
|
||||
=======
|
||||
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"
|
||||
>>>>>>> d5a288db1... feat: StudyListTable, ExpandedRow, API definitions (#1579):platform/ui/src/components/StudyListPagination/StudyListPagination.js
|
||||
style={{ height: 28 }}
|
||||
onChange={(e) => onChangePerPage(e.target.value)}
|
||||
>
|
||||
<option value="25">25</option>
|
||||
<option value="50">50</option>
|
||||
@ -82,4 +92,11 @@ const StudyListPagination = (props) => {
|
||||
);
|
||||
};
|
||||
|
||||
StudyListPagination.propTypes = {
|
||||
onChangePage: PropTypes.func.isRequired,
|
||||
currentPage: PropTypes.number.isRequired,
|
||||
perPage: PropTypes.number.isRequired,
|
||||
onChangePerPage: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default StudyListPagination;
|
||||
@ -0,0 +1,45 @@
|
||||
---
|
||||
name: Study List Pagination
|
||||
menu: Components
|
||||
route: components/studyListPagination
|
||||
---
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Playground, Props } from 'docz';
|
||||
import { StudyListPagination } from '@ohif/ui';
|
||||
|
||||
# StudyListPagination
|
||||
|
||||
## Import
|
||||
|
||||
```javascript
|
||||
import { StudyListPagination } from '@ohif/ui';
|
||||
```
|
||||
|
||||
## Basic usage
|
||||
|
||||
<Playground>
|
||||
{() => {
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [perPage, setPerPage] = useState(25);
|
||||
const onChangePage = (page) => {
|
||||
setCurrentPage(page);
|
||||
};
|
||||
const onChangePerPage = (perPage) => {
|
||||
setPerPage(perPage);
|
||||
setCurrentPage(1);
|
||||
};
|
||||
return (
|
||||
<StudyListPagination
|
||||
onChangePage={onChangePage}
|
||||
onChangePerPage={onChangePerPage}
|
||||
currentPage={currentPage}
|
||||
perPage={perPage}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
</Playground>
|
||||
|
||||
## Properties
|
||||
|
||||
<Props of={StudyListPagination} />
|
||||
3
platform/ui/src/components/StudyListPagination/index.js
Normal file
3
platform/ui/src/components/StudyListPagination/index.js
Normal file
@ -0,0 +1,3 @@
|
||||
import StudyListPagination from './StudyListPagination';
|
||||
|
||||
export default StudyListPagination;
|
||||
35
platform/ui/src/components/StudyListTable/StudyListTable.js
Normal file
35
platform/ui/src/components/StudyListTable/StudyListTable.js
Normal file
@ -0,0 +1,35 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import StudyListTableRow from './StudyListTableRow';
|
||||
|
||||
const StudyListTable = ({ tableDataSource }) => {
|
||||
return (
|
||||
<>
|
||||
<div className="bg-black">
|
||||
<div className="container m-auto relative">
|
||||
<table className="w-full text-white">
|
||||
<tbody>
|
||||
{tableDataSource.map((tableData, i) => {
|
||||
return <StudyListTableRow tableData={tableData} key={i} />;
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
StudyListTable.propTypes = {
|
||||
tableDataSource: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
row: PropTypes.array.isRequired,
|
||||
expandedContent: PropTypes.node.isRequired,
|
||||
onClickRow: PropTypes.func.isRequired,
|
||||
isExpanded: PropTypes.bool.isRequired,
|
||||
})
|
||||
),
|
||||
};
|
||||
|
||||
export default StudyListTable;
|
||||
62
platform/ui/src/components/StudyListTable/StudyListTable.mdx
Normal file
62
platform/ui/src/components/StudyListTable/StudyListTable.mdx
Normal file
@ -0,0 +1,62 @@
|
||||
---
|
||||
name: Study List Table
|
||||
menu: Components
|
||||
route: components/studyListTable
|
||||
---
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Playground, Props } from 'docz';
|
||||
import { StudyListTable } from '@ohif/ui';
|
||||
|
||||
# StudyListTable
|
||||
|
||||
This component is used to display a studies list.
|
||||
|
||||
## Import
|
||||
|
||||
```javascript
|
||||
import { StudyListTable } from '@ohif/ui';
|
||||
```
|
||||
|
||||
## Basic usage
|
||||
|
||||
<Playground>
|
||||
{() => {
|
||||
const studies = new Array(10).fill('');
|
||||
const [expandedRows, setExpandedRows] = useState([]);
|
||||
const tableDataSource = studies.map((study, key) => {
|
||||
const rowKey = key + 1;
|
||||
const isExpanded = expandedRows.some((k) => k === rowKey);
|
||||
return {
|
||||
row: [
|
||||
{ key: 'Cell 01', content: 'Cell 01', gridCol: 2 },
|
||||
{ key: 'Cell 02', content: 'Cell 02', gridCol: 4 },
|
||||
{ key: 'Cell 03', content: 'Cell 03', gridCol: 4 },
|
||||
{ key: 'Cell 04', content: 'Cell 04', gridCol: 4 },
|
||||
],
|
||||
expandedContent: (
|
||||
<td className="w-full p-8">
|
||||
Custom expanded content for row {rowKey}
|
||||
</td>
|
||||
),
|
||||
onClickRow: () =>
|
||||
setExpandedRows((rows) =>
|
||||
isExpanded ? rows.filter((n) => rowKey !== n) : [...rows, rowKey]
|
||||
),
|
||||
isExpanded,
|
||||
};
|
||||
});
|
||||
return (
|
||||
<div className="py-4">
|
||||
<StudyListTable
|
||||
tableDataSource={tableDataSource}
|
||||
numOfStudies={studies.length}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</Playground>
|
||||
|
||||
## Properties
|
||||
|
||||
<Props of={StudyListTable} />
|
||||
@ -0,0 +1,85 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
|
||||
const StudyListTableRow = (props) => {
|
||||
const { tableData } = props;
|
||||
const { row, expandedContent, onClickRow, isExpanded } = tableData;
|
||||
return (
|
||||
<>
|
||||
<tr>
|
||||
<td
|
||||
className={classnames('border-0 p-0', {
|
||||
'border-b border-custom-violetPale bg-custom-navyDark': isExpanded,
|
||||
})}
|
||||
>
|
||||
<div
|
||||
className={classnames('w-full transition duration-300', {
|
||||
'border border-custom-aquaBright rounded overflow-hidden mb-2 hover:border-custom-violetPale': isExpanded,
|
||||
})}
|
||||
>
|
||||
<table className={classnames('w-full p-4')}>
|
||||
<tbody>
|
||||
<tr
|
||||
className={classnames(
|
||||
'cursor-pointer hover:bg-custom-violetDark transition duration-300 bg-black',
|
||||
{
|
||||
'bg-custom-navyDark': !isExpanded,
|
||||
},
|
||||
{ 'bg-custom-navy': isExpanded }
|
||||
)}
|
||||
onClick={onClickRow}
|
||||
>
|
||||
{row.map((cell) => {
|
||||
const { content, gridCol, name } = cell;
|
||||
|
||||
return (
|
||||
<td
|
||||
key={name}
|
||||
className={classnames(
|
||||
'px-4 py-2 text-base',
|
||||
{ 'border-b border-custom-violetPale': !isExpanded },
|
||||
`w-${gridCol}/24` || ''
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-row items-center pl-1">
|
||||
{content}
|
||||
</div>
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
{isExpanded && (
|
||||
<tr
|
||||
className={classnames(
|
||||
'w-full bg-black max-h-0 overflow-hidden'
|
||||
)}
|
||||
>
|
||||
<td colSpan={row.length}>{expandedContent}</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
StudyListTableRow.propTypes = {
|
||||
tableData: PropTypes.shape({
|
||||
row: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
key: PropTypes.string.isRequired,
|
||||
content: PropTypes.node.isRequired,
|
||||
gridCol: PropTypes.number.isRequired,
|
||||
})
|
||||
).isRequired,
|
||||
expandedContent: PropTypes.node.isRequired,
|
||||
onClickRow: PropTypes.func.isRequired,
|
||||
isExpanded: PropTypes.bool.isRequired,
|
||||
}),
|
||||
};
|
||||
|
||||
export default StudyListTableRow;
|
||||
3
platform/ui/src/components/StudyListTable/index.js
Normal file
3
platform/ui/src/components/StudyListTable/index.js
Normal file
@ -0,0 +1,3 @@
|
||||
import StudyListTable from './StudyListTable';
|
||||
|
||||
export default StudyListTable;
|
||||
@ -14,6 +14,9 @@ import Label from './Label';
|
||||
import NavBar from './NavBar';
|
||||
import Select from './Select';
|
||||
import Svg from './Svg';
|
||||
import StudyListExpandedRow from './StudyListExpandedRow';
|
||||
import StudyListPagination from './StudyListPagination';
|
||||
import StudyListTable from './StudyListTable';
|
||||
import Table from './Table';
|
||||
import TableBody from './TableBody';
|
||||
import TableCell from './TableCell';
|
||||
@ -39,6 +42,9 @@ export {
|
||||
NavBar,
|
||||
Select,
|
||||
Svg,
|
||||
StudyListExpandedRow,
|
||||
StudyListPagination,
|
||||
StudyListTable,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
"StudyInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.78",
|
||||
"StudyDescription": "BRAIN SELLA",
|
||||
"AccessionNumber": "11788761116031",
|
||||
"StudyDate": "20010108",
|
||||
"StudyDate": "2020-03-26T23:33:59.073Z",
|
||||
"StudyTime": "120022",
|
||||
"PatientName": "MISTER^MR",
|
||||
"PatientId": "832040",
|
||||
|
||||
@ -1,3 +0,0 @@
|
||||
export default string => {
|
||||
return string.charAt(0).toUpperCase() + string.slice(1);
|
||||
};
|
||||
@ -1,15 +0,0 @@
|
||||
/**
|
||||
* Calculates the number of instances in series
|
||||
* @param {array} series
|
||||
* @returns {number} Number of instances in series
|
||||
*/
|
||||
|
||||
const getInstances = series => {
|
||||
const instances = series.reduce((acc, item) => {
|
||||
return acc + item.instances.length;
|
||||
}, 0);
|
||||
|
||||
return instances;
|
||||
};
|
||||
|
||||
export default getInstances;
|
||||
@ -1,20 +0,0 @@
|
||||
/**
|
||||
* Get formatted Modalities
|
||||
* @param {array} series
|
||||
* @returns {string} Formatted modalities
|
||||
*/
|
||||
const getModalities = series => {
|
||||
const modalities = series.reduce((acc, item) => {
|
||||
const { Modality } = item;
|
||||
if (acc.includes(Modality)) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
acc.push(Modality);
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
return modalities.join('/');
|
||||
};
|
||||
|
||||
export default getModalities;
|
||||
@ -1,10 +1,7 @@
|
||||
import capitalize from './capitalize';
|
||||
import getMockedStudies from './getMockedStudies';
|
||||
import getModalities from './getModalities';
|
||||
import getInstances from './getInstances';
|
||||
|
||||
const utils = { capitalize, getMockedStudies, getModalities, getInstances };
|
||||
const utils = { getMockedStudies };
|
||||
|
||||
export { capitalize, getMockedStudies, getModalities, getInstances };
|
||||
export { getMockedStudies };
|
||||
|
||||
export default utils;
|
||||
|
||||
@ -1,11 +1,23 @@
|
||||
/**
|
||||
* THIS IS A TEMPORARY FILE -- SHOULD BE REMOVED
|
||||
*/
|
||||
import React, { useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import moment from 'moment';
|
||||
|
||||
import {
|
||||
EmptyStudies,
|
||||
utils,
|
||||
Icon,
|
||||
StudyListExpandedRow,
|
||||
Button,
|
||||
StudyListPagination,
|
||||
StudyListTable,
|
||||
} from '@ohif/ui';
|
||||
|
||||
// fix imports after refactor
|
||||
import Header from './components/Header';
|
||||
import StudyListFilter from './components/StudyListFilter';
|
||||
import StudyListTable from './components/StudyListTable';
|
||||
import StudyListPagination from './components/StudyListPagination';
|
||||
|
||||
const filtersMeta = [
|
||||
{
|
||||
@ -84,21 +96,175 @@ const defaultFilterValues = {
|
||||
};
|
||||
|
||||
const isFiltering = (filterValues, defaultFilterValues) => {
|
||||
return Object.keys(defaultFilterValues).some(name => {
|
||||
return Object.keys(defaultFilterValues).some((name) => {
|
||||
return filterValues[name] !== defaultFilterValues[name];
|
||||
});
|
||||
};
|
||||
|
||||
const StudyList = ({ studies, perPage }) => {
|
||||
const StudyList = () => {
|
||||
const [filterValues, setFilterValues] = useState(defaultFilterValues);
|
||||
const studiesData = studies.slice(0, perPage);
|
||||
const studies = utils.getMockedStudies();
|
||||
const numOfStudies = studies.length;
|
||||
const isEmptyStudies = numOfStudies === 0;
|
||||
const [expandedRows, setExpandedRows] = useState([]);
|
||||
|
||||
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="series-active"
|
||||
className={classnames('inline-flex mr-2', {
|
||||
'text-custom-blueBright': isExpanded,
|
||||
'text-custom-violetPale': !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' }} />}
|
||||
>
|
||||
Basic Viewer
|
||||
</Button>
|
||||
<Button
|
||||
rounded="full"
|
||||
variant="contained"
|
||||
className="mr-4 font-bold"
|
||||
endIcon={<Icon name="launch-arrow" style={{ color: '#21a7c6' }} />}
|
||||
>
|
||||
Segmentation
|
||||
</Button>
|
||||
<Button
|
||||
rounded="full"
|
||||
variant="outlined"
|
||||
endIcon={<Icon name="launch-info" />}
|
||||
className="font-bold"
|
||||
>
|
||||
Module 3
|
||||
</Button>
|
||||
<div className="ml-5 text-lg text-custom-grayBright 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;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classnames('bg-black h-full', {
|
||||
'h-screen': isEmptyStudies,
|
||||
'h-screen': !hasStudies,
|
||||
})}
|
||||
>
|
||||
<Header />
|
||||
@ -110,24 +276,28 @@ const StudyList = ({ studies, perPage }) => {
|
||||
clearFilters={() => setFilterValues(defaultFilterValues)}
|
||||
isFiltering={isFiltering(filterValues, defaultFilterValues)}
|
||||
/>
|
||||
<StudyListTable
|
||||
studies={studiesData}
|
||||
numOfStudies={numOfStudies}
|
||||
filtersMeta={filtersMeta}
|
||||
/>
|
||||
{!isEmptyStudies && <StudyListPagination />}
|
||||
|
||||
{hasStudies ? (
|
||||
<>
|
||||
<StudyListTable
|
||||
tableDataSource={tableDataSource}
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
StudyList.defaultProps = {
|
||||
studies: [],
|
||||
perPage: 25,
|
||||
};
|
||||
|
||||
StudyList.propTypes = {
|
||||
studies: PropTypes.array,
|
||||
perPage: PropTypes.number,
|
||||
};
|
||||
|
||||
export default StudyList;
|
||||
|
||||
@ -4,10 +4,28 @@ menu: Views
|
||||
route: views/studyList
|
||||
---
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useState } from 'react';
|
||||
import { Playground, Props } from 'docz';
|
||||
import { StudyList, Button } from '@ohif/ui';
|
||||
import { getMockedStudies } from '../../utils/'
|
||||
import {
|
||||
StudyList,
|
||||
utils,
|
||||
Icon,
|
||||
StudyListExpandedRow,
|
||||
Button,
|
||||
NavBar,
|
||||
Svg,
|
||||
IconButton,
|
||||
EmptyStudies,
|
||||
StudyListTable,
|
||||
StudyListPagination,
|
||||
} 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 moment from 'moment';
|
||||
|
||||
# StudyList
|
||||
|
||||
@ -23,25 +41,325 @@ import { StudyList } from '@ohif/ui';
|
||||
|
||||
<Playground>
|
||||
{() => {
|
||||
const [studies, setStudies] = useState([])
|
||||
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, setStudies] = useState([]);
|
||||
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="series-active"
|
||||
className={classnames('inline-flex mr-2', {
|
||||
'text-custom-blueBright': isExpanded,
|
||||
'text-custom-violetPale': !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' }} />
|
||||
}
|
||||
>
|
||||
Basic Viewer
|
||||
</Button>
|
||||
<Button
|
||||
rounded="full"
|
||||
variant="contained"
|
||||
className="mr-4 font-bold"
|
||||
endIcon={
|
||||
<Icon name="launch-arrow" style={{ color: '#21a7c6' }} />
|
||||
}
|
||||
>
|
||||
Segmentation
|
||||
</Button>
|
||||
<Button
|
||||
rounded="full"
|
||||
variant="outlined"
|
||||
endIcon={<Icon name="launch-info" />}
|
||||
className="font-bold"
|
||||
>
|
||||
Module 3
|
||||
</Button>
|
||||
<div className="ml-5 text-lg text-custom-grayBright 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 handleStudyList = (number) => {
|
||||
const studies = getMockedStudies(number)
|
||||
setStudies(studies)
|
||||
}
|
||||
const studies = getMockedStudies(number);
|
||||
setStudies(studies);
|
||||
setCurrentPage(1);
|
||||
};
|
||||
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;
|
||||
return (
|
||||
<div>
|
||||
<div className="bg-white p-4">
|
||||
<Button className="mr-2" onClick={() => handleStudyList(0)}>0 Studies</Button>
|
||||
<Button className="mr-2" onClick={() => handleStudyList(50)}>50 Studies</Button>
|
||||
<Button className="mr-2" onClick={() => handleStudyList(100)}>100 Studies</Button>
|
||||
<Button className="mr-2" onClick={() => handleStudyList(1000)}>1000 Studies</Button>
|
||||
<Button className="mr-2" onClick={() => handleStudyList(0)}>
|
||||
0 Studies
|
||||
</Button>
|
||||
<Button className="mr-2" onClick={() => handleStudyList(50)}>
|
||||
50 Studies
|
||||
</Button>
|
||||
<Button className="mr-2" onClick={() => handleStudyList(100)}>
|
||||
100 Studies
|
||||
</Button>
|
||||
<Button className="mr-2" onClick={() => handleStudyList(1000)}>
|
||||
1000 Studies
|
||||
</Button>
|
||||
</div>
|
||||
<div
|
||||
className={classnames('bg-black h-full', {
|
||||
'h-screen': !hasStudies,
|
||||
})}
|
||||
>
|
||||
<NavBar className="justify-between border-b-4 border-black">
|
||||
<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-custom-grayLight text-lg">
|
||||
FOR INVESTIGATIONAL USE ONLY
|
||||
</span>
|
||||
<IconButton
|
||||
variant="text"
|
||||
color="inherit"
|
||||
className="text-custom-blueBright"
|
||||
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>
|
||||
<StudyList studies={studies} />
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</Playground>
|
||||
|
||||
## Properties
|
||||
|
||||
<Props of={StudyList} />
|
||||
|
||||
@ -3,125 +3,6 @@ import PropTypes from 'prop-types';
|
||||
|
||||
import { Button, Icon, Typography, InputGroup } from '@ohif/ui';
|
||||
|
||||
<<<<<<< HEAD
|
||||
const isFiltering = (currentFiltersValues, filtersValues) => {
|
||||
return Object.keys(currentFiltersValues).some((name) => {
|
||||
const filterValue = currentFiltersValues[name];
|
||||
return filterValue !== filtersValues[name];
|
||||
});
|
||||
};
|
||||
|
||||
const StudyListFilter = ({ filtersMeta, filtersValues, numOfStudies }) => {
|
||||
const [currentFiltersValues, setcurrentFiltersValues] = useState(
|
||||
filtersValues
|
||||
);
|
||||
const { sortBy, sortDirection } = currentFiltersValues;
|
||||
|
||||
const handleFilterLabelClick = (name) => {
|
||||
let _sortDirection = 'ascending';
|
||||
if (sortBy === name) {
|
||||
if (sortDirection === 'ascending') {
|
||||
_sortDirection = 'descending';
|
||||
} else if (sortDirection === 'descending') {
|
||||
_sortDirection = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
if (numOfStudies <= 100) {
|
||||
setcurrentFiltersValues((prevState) => ({
|
||||
...prevState,
|
||||
sortBy: _sortDirection !== 0 ? name : '',
|
||||
sortDirection: _sortDirection,
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
const clearFilters = () => {
|
||||
setcurrentFiltersValues(filtersValues);
|
||||
};
|
||||
|
||||
const renderFieldInputComponent = ({
|
||||
name,
|
||||
displayName,
|
||||
inputProps,
|
||||
isSortable,
|
||||
inputType,
|
||||
}) => {
|
||||
const _isSortable = isSortable && numOfStudies <= 100 && numOfStudies > 0;
|
||||
const _sortDirection = sortBy !== name ? 'none' : sortDirection;
|
||||
const onLabelClick = () => {
|
||||
if (_isSortable) {
|
||||
handleFilterLabelClick(name);
|
||||
}
|
||||
};
|
||||
switch (inputType) {
|
||||
case 'Text':
|
||||
return (
|
||||
<InputText
|
||||
key={name}
|
||||
label={displayName}
|
||||
isSortable={_isSortable}
|
||||
sortDirection={_sortDirection}
|
||||
onLabelClick={onLabelClick}
|
||||
value={currentFiltersValues[name]}
|
||||
onChange={(newValue) => {
|
||||
setcurrentFiltersValues((prevState) => ({
|
||||
...prevState,
|
||||
[name]: newValue,
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
case 'MultiSelect':
|
||||
return (
|
||||
<InputMultiSelect
|
||||
key={name}
|
||||
label={displayName}
|
||||
isSortable={_isSortable}
|
||||
sortDirection={_sortDirection}
|
||||
onLabelClick={onLabelClick}
|
||||
options={inputProps.options}
|
||||
value={currentFiltersValues[name]}
|
||||
onChange={(newValue) => {
|
||||
setcurrentFiltersValues((prevState) => ({
|
||||
...prevState,
|
||||
[name]: newValue,
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
);
|
||||
case 'DateRange':
|
||||
return (
|
||||
<InputDateRange
|
||||
key={name}
|
||||
label={displayName}
|
||||
isSortable={_isSortable}
|
||||
sortDirection={_sortDirection}
|
||||
onLabelClick={onLabelClick}
|
||||
value={currentFiltersValues[name]}
|
||||
onChange={(newValue) => {
|
||||
setcurrentFiltersValues((prevState) => ({
|
||||
...prevState,
|
||||
[name]: newValue,
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
);
|
||||
case 'None':
|
||||
return (
|
||||
<InputLabelWrapper
|
||||
key={name}
|
||||
label={displayName}
|
||||
isSortable={_isSortable}
|
||||
sortDirection={_sortDirection}
|
||||
onLabelClick={onLabelClick}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
break;
|
||||
}
|
||||
=======
|
||||
const StudyListFilter = ({
|
||||
filtersMeta,
|
||||
filterValues,
|
||||
@ -137,7 +18,6 @@ const StudyListFilter = ({
|
||||
...filterValues,
|
||||
...sortingValues,
|
||||
});
|
||||
>>>>>>> 26d8c8ae5... refactor: StudyListFilter refactor (#1580)
|
||||
};
|
||||
const isSortingEnable = numOfStudies > 0 && numOfStudies <= 100;
|
||||
|
||||
@ -194,26 +74,6 @@ const StudyListFilter = ({
|
||||
</div>
|
||||
</div>
|
||||
<div className="sticky z-10 border-b-4 border-black" style={{ top: 58 }}>
|
||||
<<<<<<< HEAD
|
||||
<div className="bg-primary-dark pt-3 pb-3 ">
|
||||
<div className="container m-auto relative flex flex-col">
|
||||
<div className="flex flex-row w-full">
|
||||
{filtersMeta.map((filterMeta) => {
|
||||
return (
|
||||
<div
|
||||
key={filterMeta.name}
|
||||
className={classnames(
|
||||
'pl-4 first:pl-12',
|
||||
`w-${filterMeta.gridCol}/24`
|
||||
)}
|
||||
>
|
||||
{renderFieldInputComponent(filterMeta)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
=======
|
||||
<div className="bg-custom-navyDark pt-3 pb-3 ">
|
||||
<InputGroup
|
||||
inputMeta={filtersMeta}
|
||||
@ -223,7 +83,6 @@ const StudyListFilter = ({
|
||||
onSortingChange={setFilterSorting}
|
||||
isSortingEnable={isSortingEnable}
|
||||
/>
|
||||
>>>>>>> 26d8c8ae5... refactor: StudyListFilter refactor (#1580)
|
||||
</div>
|
||||
{numOfStudies > 100 && (
|
||||
<div className="container m-auto">
|
||||
|
||||
@ -1,12 +1,14 @@
|
||||
import React from 'react';
|
||||
import { StudyList } from '@ohif/ui';
|
||||
|
||||
// TEMPORARY MOCKING DATA FOR VISUALIZATION PURPOSES
|
||||
import { utils } from '@ohif/ui';
|
||||
|
||||
const ConnectedStudyList = () => {
|
||||
const studies = utils.getMockedStudies();
|
||||
return <StudyList studies={studies} />;
|
||||
return (
|
||||
/** Temporary using StudyList -- We should mount it by using the created
|
||||
* components such as NavBar, InputGroup, StudyListTable, etc...
|
||||
* Check "Views > StudyList" in @ohif/ui for reference
|
||||
*/
|
||||
<StudyList />
|
||||
);
|
||||
};
|
||||
|
||||
export default ConnectedStudyList;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user