Reorganize routes folder
This commit is contained in:
1 parent
ea50ade126
commit
3880b874ab
11 files changed
+66
-55
No files matched your search
@@ -0,0 +1,466 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import classnames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Link } from 'react-router-dom';
|
||||
import moment from 'moment';
|
||||
import qs from 'query-string';
|
||||
//
|
||||
import filtersMeta from './filtersMeta.js';
|
||||
import { useDebounce, useQuery } from './../../hooks';
|
||||
|
||||
import {
|
||||
Icon,
|
||||
StudyListExpandedRow,
|
||||
Button,
|
||||
NavBar,
|
||||
Svg,
|
||||
IconButton,
|
||||
EmptyStudies,
|
||||
StudyListTable,
|
||||
StudyListPagination,
|
||||
StudyListFilter,
|
||||
} from '@ohif/ui';
|
||||
|
||||
const seriesInStudiesMap = new Map();
|
||||
|
||||
/**
|
||||
* TODO:
|
||||
* - debounce `setFilterValues` (150ms?)
|
||||
*/
|
||||
function WorkList({ history, data: studies, dataSource }) {
|
||||
// ~ Filters
|
||||
const query = useQuery();
|
||||
const queryFilterValues = _getQueryFilterValues(query);
|
||||
const [filterValues, _setFilterValues] = useState(
|
||||
Object.assign({}, defaultFilterValues, queryFilterValues)
|
||||
);
|
||||
const debouncedFilterValues = useDebounce(filterValues, 200);
|
||||
const { resultsPerPage, pageNumber, sortBy, sortDirection } = filterValues;
|
||||
|
||||
const sortedStudies = studies
|
||||
// TOOD: Move sort to DataSourceWrapper?
|
||||
// TODO: MOTIVATION, this is triggered on every render, even if list/sort does not change
|
||||
.sort((s1, s2) => {
|
||||
const noSortApplied = sortBy === '' || !sortBy;
|
||||
const sortModifier = sortDirection === 'descending' ? 1 : -1;
|
||||
if (noSortApplied) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const s1Prop = s1[sortBy];
|
||||
const s2Prop = s2[sortBy];
|
||||
|
||||
if (typeof s1Prop === 'string' && typeof s2Prop === 'string') {
|
||||
return s1Prop.localeCompare(s2Prop) * sortModifier;
|
||||
} else if (typeof s1Prop === 'number' && typeof s2Prop === 'number') {
|
||||
return (s1Prop > s2Prop ? 1 : -1) * sortModifier;
|
||||
} else if (!s1Prop && s2Prop) {
|
||||
return -1 * sortModifier;
|
||||
} else if (!s2Prop && s1Prop) {
|
||||
return 1 * sortModifier;
|
||||
} else if (sortBy === 'studyDate') {
|
||||
// TODO: Delimiters are non-standard. Should we support them?
|
||||
const s1Date = moment(s1.date, ['YYYYMMDD', 'YYYY.MM.DD'], true);
|
||||
const s2Date = moment(s2.date, ['YYYYMMDD', 'YYYY.MM.DD'], true);
|
||||
|
||||
if (s1Date.isValid() && s2Date.isValid()) {
|
||||
return (
|
||||
(s1Date.toISOString() > s2Date.toISOString() ? 1 : -1) *
|
||||
sortModifier
|
||||
);
|
||||
} else if (s1Date.isValid()) {
|
||||
return sortModifier;
|
||||
} else if (s2Date.isValid()) {
|
||||
return -1 * sortModifier;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
});
|
||||
// ~ Rows & Studies
|
||||
const [expandedRows, setExpandedRows] = useState([]);
|
||||
const [studiesWithSeriesData, setStudiesWithSeriesData] = useState([]);
|
||||
const numOfStudies = studies.length;
|
||||
const totalPages = Math.floor(numOfStudies / resultsPerPage);
|
||||
|
||||
const setFilterValues = val => {
|
||||
if (filterValues.pageNumber === val.pageNumber) {
|
||||
val.pageNumber = 1;
|
||||
}
|
||||
_setFilterValues(val);
|
||||
setExpandedRows([]);
|
||||
};
|
||||
|
||||
const onPageNumberChange = newPageNumber => {
|
||||
if (newPageNumber > totalPages) {
|
||||
return;
|
||||
}
|
||||
setFilterValues({ ...filterValues, pageNumber: newPageNumber });
|
||||
};
|
||||
|
||||
const onResultsPerPageChange = newResultsPerPage => {
|
||||
setFilterValues({
|
||||
...filterValues,
|
||||
pageNumber: 1,
|
||||
resultsPerPage: Number(newResultsPerPage),
|
||||
});
|
||||
};
|
||||
|
||||
// Set body style
|
||||
useEffect(() => {
|
||||
document.body.classList.add('bg-black');
|
||||
return () => {
|
||||
document.body.classList.remove('bg-black');
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Sync URL query parameters with filters
|
||||
useEffect(() => {
|
||||
if (!debouncedFilterValues) {
|
||||
return;
|
||||
}
|
||||
const queryString = {};
|
||||
Object.keys(defaultFilterValues).forEach(key => {
|
||||
const defaultValue = defaultFilterValues[key];
|
||||
const currValue = debouncedFilterValues[key];
|
||||
|
||||
// TODO: nesting/recursion?
|
||||
if (key === 'studyDate') {
|
||||
if (
|
||||
currValue.startDate &&
|
||||
defaultValue.startDate !== currValue.startDate
|
||||
) {
|
||||
queryString.startDate = currValue.startDate;
|
||||
}
|
||||
if (currValue.endDate && defaultValue.endDate !== currValue.endDate) {
|
||||
queryString.endDate = currValue.endDate;
|
||||
}
|
||||
} else if (key === 'modalities' && currValue.length) {
|
||||
queryString.modalities = currValue.join(',');
|
||||
} else if (currValue !== defaultValue) {
|
||||
queryString[key] = currValue;
|
||||
}
|
||||
});
|
||||
|
||||
history.push({
|
||||
pathname: '/',
|
||||
search: `?${qs.stringify(queryString, {
|
||||
skipNull: true,
|
||||
skipEmptyString: true,
|
||||
})}`,
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [debouncedFilterValues]);
|
||||
|
||||
// Query for series information
|
||||
useEffect(() => {
|
||||
const fetchSeries = async studyInstanceUid => {
|
||||
try {
|
||||
const result = await dataSource.query.series.search(studyInstanceUid);
|
||||
|
||||
seriesInStudiesMap.set(studyInstanceUid, result);
|
||||
setStudiesWithSeriesData([...studiesWithSeriesData, studyInstanceUid]);
|
||||
} catch (ex) {
|
||||
// TODO: UI Notification Service
|
||||
console.warn(ex);
|
||||
}
|
||||
};
|
||||
|
||||
// TODO: WHY WOULD YOU USE AN INDEX OF 1?!
|
||||
// Note: expanded rows index begins at 1
|
||||
for (let z = 0; z < expandedRows.length; z++) {
|
||||
const expandedRowIndex = expandedRows[z] - 1;
|
||||
console.log(sortedStudies[expandedRowIndex]);
|
||||
const studyInstanceUid = sortedStudies[expandedRowIndex].studyInstanceUid;
|
||||
if (studiesWithSeriesData.includes(studyInstanceUid)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
console.log(`fetching for ${expandedRowIndex}`)
|
||||
fetchSeries(studyInstanceUid);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [expandedRows, studies]);
|
||||
|
||||
const isFiltering = (filterValues, defaultFilterValues) => {
|
||||
return Object.keys(defaultFilterValues).some(name => {
|
||||
return filterValues[name] !== defaultFilterValues[name];
|
||||
});
|
||||
};
|
||||
const tableDataSource = sortedStudies.map((study, key) => {
|
||||
const rowKey = key + 1;
|
||||
const isExpanded = expandedRows.some(k => k === rowKey);
|
||||
const {
|
||||
studyInstanceUid,
|
||||
accession,
|
||||
modalities,
|
||||
instances,
|
||||
description,
|
||||
mrn,
|
||||
patientName,
|
||||
date,
|
||||
time,
|
||||
} = study;
|
||||
return {
|
||||
row: [
|
||||
{
|
||||
key: 'patientName',
|
||||
content: patientName ? (
|
||||
patientName
|
||||
) : (
|
||||
<span className="text-gray-700">(Empty)</span>
|
||||
),
|
||||
title: patientName,
|
||||
gridCol: 4,
|
||||
},
|
||||
{
|
||||
key: 'mrn',
|
||||
content: mrn,
|
||||
title: mrn,
|
||||
gridCol: 2,
|
||||
},
|
||||
{
|
||||
key: 'studyDate',
|
||||
content: (
|
||||
<div>
|
||||
<span className="mr-4">
|
||||
{date &&
|
||||
moment(date, ['YYYYMMDD', 'YYYY.MM.DD'], true).isValid() &&
|
||||
moment(date, ['YYYYMMDD', 'YYYY.MM.DD']).format(
|
||||
'MMM-DD-YYYY'
|
||||
)}
|
||||
</span>
|
||||
{time && (
|
||||
<span>
|
||||
{time &&
|
||||
moment(time, [
|
||||
'HH',
|
||||
'HHmm',
|
||||
'HHmmss',
|
||||
'HHmmss.SSS',
|
||||
]).isValid() &&
|
||||
moment(time, ['HH', 'HHmm', 'HHmmss', 'HHmmss.SSS']).format(
|
||||
'hh:mm A'
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
title: 'time',
|
||||
gridCol: 5,
|
||||
},
|
||||
{
|
||||
key: 'description',
|
||||
content: description,
|
||||
title: description,
|
||||
gridCol: 4,
|
||||
},
|
||||
{
|
||||
key: 'modality',
|
||||
content: modalities,
|
||||
title: modalities,
|
||||
gridCol: 3,
|
||||
},
|
||||
{
|
||||
key: 'accession',
|
||||
content: accession,
|
||||
title: accession,
|
||||
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}
|
||||
</>
|
||||
),
|
||||
title: (instances || 0).toString(),
|
||||
gridCol: 4,
|
||||
},
|
||||
],
|
||||
expandedContent: (
|
||||
<StudyListExpandedRow
|
||||
seriesTableColumns={{
|
||||
description: 'Description',
|
||||
seriesNumber: 'Series',
|
||||
modality: 'Modality',
|
||||
instances: 'Instances',
|
||||
}}
|
||||
seriesTableDataSource={
|
||||
seriesInStudiesMap.has(studyInstanceUid)
|
||||
? seriesInStudiesMap.get(studyInstanceUid).map(s => {
|
||||
return {
|
||||
description: s.description || '(empty)',
|
||||
seriesNumber: s.seriesNumber || '',
|
||||
modality: s.modality || '',
|
||||
instances: s.numSeriesInstances || '',
|
||||
};
|
||||
})
|
||||
: []
|
||||
}
|
||||
>
|
||||
<Button
|
||||
rounded="full"
|
||||
variant="outlined"
|
||||
disabled={true}
|
||||
endIcon={<Icon name="launch-info" />}
|
||||
className="font-bold"
|
||||
>
|
||||
Comparison
|
||||
</Button>
|
||||
<Button
|
||||
rounded="full"
|
||||
variant="outlined"
|
||||
endIcon={<Icon name="launch-info" />}
|
||||
className="font-bold ml-4"
|
||||
>
|
||||
View: Segmentation
|
||||
</Button>
|
||||
</StudyListExpandedRow>
|
||||
),
|
||||
onClickRow: () =>
|
||||
setExpandedRows(s =>
|
||||
isExpanded ? s.filter(n => rowKey !== n) : [...s, rowKey]
|
||||
),
|
||||
isExpanded,
|
||||
};
|
||||
});
|
||||
|
||||
const hasStudies = numOfStudies > 0;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classnames('bg-black h-full', {
|
||||
'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}
|
||||
onChange={setFilterValues}
|
||||
clearFilters={() => setFilterValues(defaultFilterValues)}
|
||||
isFiltering={isFiltering(filterValues, defaultFilterValues)}
|
||||
/>
|
||||
{hasStudies ? (
|
||||
<>
|
||||
<StudyListTable
|
||||
tableDataSource={tableDataSource.slice(
|
||||
(pageNumber - 1) * resultsPerPage,
|
||||
(pageNumber - 1) * resultsPerPage + resultsPerPage
|
||||
)}
|
||||
numOfStudies={numOfStudies}
|
||||
filtersMeta={filtersMeta}
|
||||
/>
|
||||
<StudyListPagination
|
||||
onChangePage={onPageNumberChange}
|
||||
onChangePerPage={onResultsPerPageChange}
|
||||
currentPage={pageNumber}
|
||||
perPage={resultsPerPage}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center pt-48">
|
||||
<EmptyStudies />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
WorkList.propTypes = {
|
||||
history: PropTypes.shape({
|
||||
push: PropTypes.func,
|
||||
}).isRequired,
|
||||
data: PropTypes.array.isRequired,
|
||||
dataSource: PropTypes.shape({
|
||||
query: PropTypes.object.isRequired,
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
const defaultFilterValues = {
|
||||
patientName: '',
|
||||
mrn: '',
|
||||
studyDate: {
|
||||
startDate: undefined,
|
||||
endDate: undefined,
|
||||
},
|
||||
description: '',
|
||||
modalities: [],
|
||||
accession: '',
|
||||
sortBy: '',
|
||||
sortDirection: 'none',
|
||||
pageNumber: 1,
|
||||
resultsPerPage: 25,
|
||||
};
|
||||
|
||||
function _getQueryFilterValues(query) {
|
||||
const queryFilterValues = {
|
||||
patientName: query.get('patientName'),
|
||||
mrn: query.get('mrn'),
|
||||
studyDate: {
|
||||
startDate: query.get('startDate'),
|
||||
endDate: query.get('endDate'),
|
||||
},
|
||||
description: query.get('description'),
|
||||
modalities: query.get('modalities')
|
||||
? query.get('modalities').split(',')
|
||||
: [],
|
||||
accession: query.get('accession'),
|
||||
sortBy: query.get('sortBy'),
|
||||
sortDirection: query.get('sortDirection'),
|
||||
pageNumber: _tryParseInt(query.get('pageNumber'), undefined),
|
||||
resultsPerPage: _tryParseInt(query.get('resultsPerPage'), undefined),
|
||||
};
|
||||
|
||||
// Delete null/undefined keys
|
||||
Object.keys(queryFilterValues).forEach(
|
||||
key => queryFilterValues[key] == null && delete queryFilterValues[key]
|
||||
);
|
||||
|
||||
return queryFilterValues;
|
||||
|
||||
function _tryParseInt(str, defaultValue) {
|
||||
var retValue = defaultValue;
|
||||
if (str !== null) {
|
||||
if (str.length > 0) {
|
||||
if (!isNaN(str)) {
|
||||
retValue = parseInt(str);
|
||||
}
|
||||
}
|
||||
}
|
||||
return retValue;
|
||||
}
|
||||
}
|
||||
|
||||
export default WorkList;
|
||||
@@ -0,0 +1,61 @@
|
||||
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: 'modalities',
|
||||
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,
|
||||
},
|
||||
];
|
||||
|
||||
export default filtersMeta;
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './WorkList';
|
||||
Reference in new issue
Block a user