diff --git a/platform/viewer/src/hooks/useDebounce.js b/platform/viewer/src/hooks/useDebounce.js new file mode 100644 index 000000000..45dde5b9e --- /dev/null +++ b/platform/viewer/src/hooks/useDebounce.js @@ -0,0 +1,31 @@ +import { useState, useEffect } from 'react'; + +/** + * See: https://usehooks.com/useDebounce/ + * + * @param {*} value + * @param {number} delay - delat in ms + */ +export default function useDebounce(value, delay) { + // State and setters for debounced value + const [debouncedValue, setDebouncedValue] = useState(value); + + useEffect( + () => { + // Update debounced value after delay + const handler = setTimeout(() => { + setDebouncedValue(value); + }, delay); + + // Cancel the timeout if value changes (also on delay change or unmount) + // This is how we prevent debounced value from updating if value is changed ... + // .. within the delay period. Timeout gets cleared and restarted. + return () => { + clearTimeout(handler); + }; + }, + [value, delay] // Only re-call effect if value or delay changes + ); + + return debouncedValue; +} diff --git a/platform/viewer/src/routes/StudyListContainer/StudyListContainer.jsx b/platform/viewer/src/routes/StudyListContainer/StudyListContainer.jsx index d46b3eb5d..5a74cb1cb 100644 --- a/platform/viewer/src/routes/StudyListContainer/StudyListContainer.jsx +++ b/platform/viewer/src/routes/StudyListContainer/StudyListContainer.jsx @@ -1,10 +1,13 @@ 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 { useDebounce, useQuery } from './../../hooks'; import { - utils, Icon, StudyListExpandedRow, Button, @@ -17,31 +20,50 @@ import { StudyListFilter, } from '@ohif/ui'; -// URL Query Hook -import { useQuery } from '../../hooks'; - -function StudyListContainer() { +/** + * TODO: + * - debounce `setFilterValues` (150ms?) + */ +function StudyListContainer({ history, studies }) { + // ~ Filters const query = useQuery(); - - 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 queryFilterValues = _getQueryFilterValues(query); + const [filterValues, setFilterValues] = useState( + Object.assign({}, defaultFilterValues, queryFilterValues) + ); + const debouncedFilterValues = useDebounce(filterValues, 500); + // ~ Rows & Studies const [expandedRows, setExpandedRows] = useState([]); + const numOfStudies = studies.length; + + useEffect(() => { + if (!debouncedFilterValues) { + return; + } + const queryString = {}; + Object.keys(defaultFilterValues).forEach(key => { + const defaultValue = defaultFilterValues[key]; + const currValue = debouncedFilterValues[key]; + + if ( + typeof currValue === 'object' && + currValue !== null && + JSON.stringify(currValue) !== JSON.stringify(defaultValue) + ) { + queryString[key] = JSON.stringify(currValue); + } else if (currValue !== defaultValue) { + queryString[key] = currValue; + } + }); + history.push({ + pathname: '/', + search: `?${qs.stringify(queryString, { + skipNull: true, + skipEmptyString: true, + })}`, + }); + }, [debouncedFilterValues, history]); + const filtersMeta = [ { name: 'patientName', @@ -259,10 +281,6 @@ function StudyListContainer() { const hasStudies = numOfStudies > 0; - useEffect(() => { - query.setQueryString(filterValues); - }, [filterValues, query]); - return (
setFilterValues(defaultFilterValues)} isFiltering={isFiltering(filterValues, defaultFilterValues)} /> @@ -326,4 +344,85 @@ function StudyListContainer() { ); } +StudyListContainer.propTypes = { + history: propTypes.shape({ + push: propTypes.func, + }), + studies: propTypes.array, +}; + +StudyListContainer.defaultProps = { + studies: [], +}; + +const defaultFilterValues = { + patientName: '', + mrn: '', + studyDate: { + startDate: null, + endDate: null, + }, + description: '', + modality: [], + accession: '', + sortBy: '', + sortDirection: 'none', + page: 0, + resultsPerPage: 25, +}; + +function _getQueryFilterValues(query) { + const queryFilterValues = { + patientName: query.get('patientName'), + mrn: query.get('mrn'), + studyDate: _tryParseDates(query.get('studyDate'), undefined), + description: query.get('description'), + modality: _tryParseJson(query.get('modality'), undefined), + accession: query.get('accession'), + sortBy: query.get('soryBy'), + sortDirection: query.get('sortDirection'), + page: _tryParseInt(query.get('page'), 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; + } + + function _tryParseJson(str, defaultValue) { + return str ? JSON.parse(str) : defaultValue; + } + + function _tryParseDates(str, defaultValue) { + const studyDateObject = _tryParseJson(str, defaultValue); + + if (studyDateObject) { + studyDateObject.startDate = studyDateObject.startDate + ? moment(new Date(studyDateObject.startDate)) + : undefined; + + studyDateObject.endDate = studyDateObject.endDate + ? moment(new Date(studyDateObject.endDate)) + : undefined; + } + + return studyDateObject; + } +} + export default StudyListContainer;