diff --git a/platform/ui/src/components/studyList/StudyList.js b/platform/ui/src/components/studyList/StudyList.js
index 5ba9d7ee0..4565c9823 100644
--- a/platform/ui/src/components/studyList/StudyList.js
+++ b/platform/ui/src/components/studyList/StudyList.js
@@ -3,11 +3,22 @@ import './StudyList.styl';
import React from 'react';
import classNames from 'classnames';
import TableSearchFilter from './TableSearchFilter.js';
-import useMedia from '../../hooks/useMedia.js';
import PropTypes from 'prop-types';
import { StudyListLoadingText } from './StudyListLoadingText.js';
-import { withTranslation } from '../../contextProviders';
+import { useTranslation } from 'react-i18next';
+const getContentFromUseMediaValue = (
+ displaySize,
+ contentArrayMap,
+ defaultContent
+) => {
+ const content =
+ displaySize in contentArrayMap
+ ? contentArrayMap[displaySize]
+ : defaultContent;
+
+ return content;
+};
/**
*
*
@@ -24,9 +35,10 @@ function StudyList(props) {
filterValues,
onFilterChange: handleFilterChange,
onSelectItem: handleSelectItem,
- t,
studyListDateFilterNumDays,
+ displaySize,
} = props;
+ const { t, ready: translationsAreReady } = useTranslation('StudyList');
const largeTableMeta = [
{
@@ -69,13 +81,13 @@ function StudyList(props) {
const mediumTableMeta = [
{
- displayText: 'Patient / MRN',
+ displayText: `${t('Patient')} / ${t('MRN')}`,
fieldName: 'patientNameOrId',
inputType: 'text',
size: 250,
},
{
- displayText: 'Description',
+ displayText: t('Description'),
fieldName: 'accessionOrModalityOrDescription',
inputType: 'text',
size: 350,
@@ -90,16 +102,16 @@ function StudyList(props) {
const smallTableMeta = [
{
- displayText: 'Search',
+ displayText: t('Search'),
fieldName: 'allFields',
inputType: 'text',
size: 100,
},
];
- const tableMeta = useMedia(
- ['(min-width: 1750px)', '(min-width: 1000px)', '(min-width: 768px)'],
- [largeTableMeta, mediumTableMeta, smallTableMeta],
+ const tableMeta = getContentFromUseMediaValue(
+ displaySize,
+ { large: largeTableMeta, medium: mediumTableMeta, small: smallTableMeta },
smallTableMeta
);
@@ -107,7 +119,7 @@ function StudyList(props) {
.map(field => field.size)
.reduce((prev, next) => prev + next);
- return (
+ return translationsAreReady ? (
{tableMeta.map((field, i) => {
@@ -176,11 +188,12 @@ function StudyList(props) {
studyDescription={study.studyDescription || ''}
studyInstanceUid={study.studyInstanceUid}
t={t}
+ displaySize={displaySize}
/>
))}
- );
+ ) : null;
}
StudyList.propTypes = {
@@ -205,9 +218,12 @@ StudyList.propTypes = {
patientNameOrId: PropTypes.string.isRequired,
accessionOrModalityOrDescription: PropTypes.string.isRequired,
allFields: PropTypes.string.isRequired,
+ studyDateTo: PropTypes.any,
+ studyDateFrom: PropTypes.any,
}).isRequired,
onFilterChange: PropTypes.func.isRequired,
studyListDateFilterNumDays: PropTypes.number,
+ displaySize: PropTypes.string,
};
StudyList.defaultProps = {};
@@ -224,6 +240,7 @@ function TableRow(props) {
studyInstanceUid,
onClick: handleClick,
t,
+ displaySize,
} = props;
const largeRowTemplate = (
@@ -360,9 +377,13 @@ function TableRow(props) {
);
- const rowTemplate = useMedia(
- ['(min-width: 1750px)', '(min-width: 1000px)', '(min-width: 768px)'],
- [largeRowTemplate, mediumRowTemplate, smallRowTemplate],
+ const rowTemplate = getContentFromUseMediaValue(
+ displaySize,
+ {
+ large: largeRowTemplate,
+ medium: mediumRowTemplate,
+ small: smallRowTemplate,
+ },
smallRowTemplate
);
@@ -378,11 +399,11 @@ TableRow.propTypes = {
studyDate: PropTypes.string.isRequired,
studyDescription: PropTypes.string.isRequired,
studyInstanceUid: PropTypes.string.isRequired,
+ displaySize: PropTypes.string,
};
TableRow.defaultProps = {
isHighlighted: false,
};
-const connectedComponent = withTranslation('StudyList')(StudyList);
-export { connectedComponent as StudyList };
+export { StudyList };
diff --git a/platform/ui/src/components/studyList/TableSearchFilter.js b/platform/ui/src/components/studyList/TableSearchFilter.js
index e3a2aa514..396738db9 100644
--- a/platform/ui/src/components/studyList/TableSearchFilter.js
+++ b/platform/ui/src/components/studyList/TableSearchFilter.js
@@ -6,6 +6,22 @@ import CustomDateRangePicker from './CustomDateRangePicker.js';
import { Icon } from './../../elements/Icon';
import { useTranslation } from 'react-i18next';
+const getDateEntry = (datePicked, rangeDatePicked) => {
+ return rangeDatePicked || datePicked || null;
+};
+
+const getDateEntryFromRange = (today, numOfDays, edge = 'start') => {
+ if (typeof numOfDays !== 'number') {
+ return;
+ }
+
+ if (edge === 'end') {
+ return today;
+ } else {
+ today.subtract(numOfDays, 'days');
+ }
+};
+
function TableSearchFilter(props) {
const {
meta,
@@ -17,20 +33,30 @@ function TableSearchFilter(props) {
// TODO: Rename
studyListDateFilterNumDays,
} = props;
+
+ const { studyDateTo, studyDateFrom } = values || {};
const [focusedInput, setFocusedInput] = useState(null);
- const [t] = useTranslation(); // 'Common'?
+ const { t, ready: translationsAreReady } = useTranslation('Common');
const sortIcons = ['sort', 'sort-up', 'sort-down'];
const sortIconForSortField =
sortDirection === 'asc' ? sortIcons[1] : sortIcons[2];
+
const today = moment();
const lastWeek = moment().subtract(7, 'day');
const lastMonth = moment().subtract(1, 'month');
- const defaultStartDate = moment().subtract(
+
+ const defaultStartDate = getDateEntryFromRange(
+ today,
studyListDateFilterNumDays,
- 'days'
+ 'start'
);
- const defaultEndDate = today;
+ const defaultEndDate = getDateEntryFromRange(
+ today,
+ studyListDateFilterNumDays,
+ 'end'
+ );
+
const studyDatePresets = [
{
text: t('Today'),
@@ -49,56 +75,59 @@ function TableSearchFilter(props) {
},
];
- return meta.map((field, i) => {
- const { displayText, fieldName, inputType } = field;
- const isSortField = sortFieldName === fieldName;
- const sortIcon = isSortField ? sortIconForSortField : sortIcons[0];
+ return translationsAreReady
+ ? meta.map((field, i) => {
+ const { displayText, fieldName, inputType } = field;
+ const isSortField = sortFieldName === fieldName;
+ const sortIcon = isSortField ? sortIconForSortField : sortIcons[0];
- return (
-
-
- {inputType === 'text' && (
- onValueChange(fieldName, e.target.value)}
- />
- )}
- {inputType === 'date-range' && (
- // https://github.com/airbnb/react-dates
- {
- onValueChange('studyDateFrom', startDate);
- onValueChange('studyDateTo', endDate);
- }}
- focusedInput={focusedInput}
- onFocusChange={updatedVal => setFocusedInput(updatedVal)}
- // Optional
- numberOfMonths={1} // For med and small screens? 2 for large?
- showClearDates={true}
- anchorDirection="left"
- presets={studyDatePresets}
- hideKeyboardShortcutsPanel={true}
- isOutsideRange={day => !isInclusivelyBeforeDay(day, moment())}
- />
- )}
- |
- );
- });
+ return (
+
+
+ {inputType === 'text' && (
+ onValueChange(fieldName, e.target.value)}
+ />
+ )}
+ {inputType === 'date-range' && (
+ // https://github.com/airbnb/react-dates
+ {
+ onValueChange('studyDateTo', startDate);
+ onValueChange('studyDateFrom', endDate);
+ }}
+ focusedInput={focusedInput}
+ onFocusChange={updatedVal => setFocusedInput(updatedVal)}
+ // Optional
+ numberOfMonths={1} // For med and small screens? 2 for large?
+ showClearDates={true}
+ anchorDirection="left"
+ presets={studyDatePresets}
+ hideKeyboardShortcutsPanel={true}
+ isOutsideRange={day => !isInclusivelyBeforeDay(day, moment())}
+ />
+ )}
+ |
+ );
+ })
+ : null;
}
TableSearchFilter.propTypes = {
diff --git a/platform/ui/src/hooks/index.js b/platform/ui/src/hooks/index.js
index 332bc509b..605d609c0 100644
--- a/platform/ui/src/hooks/index.js
+++ b/platform/ui/src/hooks/index.js
@@ -1,4 +1,4 @@
-import useMedia from './useMedia.js';
+import { useMedia } from './useMedia.js';
import useDebounce from './useDebounce.js';
export { useDebounce, useMedia };
diff --git a/platform/ui/src/hooks/useMedia.js b/platform/ui/src/hooks/useMedia.js
index 1c6ab0c29..0f582902d 100644
--- a/platform/ui/src/hooks/useMedia.js
+++ b/platform/ui/src/hooks/useMedia.js
@@ -1,54 +1,178 @@
-import { useState, useEffect } from 'react';
+import { useState, useEffect, useRef, useCallback } from 'react';
+import isEqual from 'lodash.isequal';
+/**
+ * Get display size value for matched mediaQueryList
+ * @param {MediaQueryList[]} mediaQueryMap - Array of mappings, containing MediaQueryLists
+ * @param {Array} mediaTypesAliases - Array of strings representing each mediaQueryAlias.
+ * @param {string} defaultDisplaySize - default display size value. Fallback value.
+ */
+const getDisplaySize = (
+ mediaQueryMap,
+ mediaTypesAliases,
+ defaultDisplaySize
+) => {
+ if ((!mediaTypesAliases && !defaultDisplaySize) || !mediaQueryMap) {
+ return;
+ }
+
+ // Get index of first media query that matches
+ const index = mediaQueryMap.findIndex(mql => mql.matches);
+
+ // Return related value or defaultDisplaySize if none
+ return index >= 0 && typeof mediaTypesAliases[index] !== 'undefined'
+ ? mediaTypesAliases[index]
+ : defaultDisplaySize;
+};
+/**
+ * Map each window MediaQueryLists
+ * @param {Array} mediaQueriesStringList - array of string media queries to be parsed
+ */
+const getMediaQueryMap = mediaQueriesStringList => {
+ return (
+ mediaQueriesStringList &&
+ mediaQueriesStringList.map(q => window.matchMedia(q))
+ );
+};
+
+const getMediaTypeAlias = (mediaQuery, state) => {
+ const { media } = mediaQuery;
+ const { mediaQueriesStringList, mediaTypesAliases } = state;
+
+ const index = mediaQueriesStringList.findIndex(originalMediaQuery => {
+ const { media: toCompareMedia } = window.matchMedia(originalMediaQuery);
+ return toCompareMedia === media;
+ });
+
+ return mediaTypesAliases[index];
+};
/**
+ * Hook to get current displaySize value.
+ *
+ * Its state changes and also displaySize value changes in case viewport is resized.
+ * Its state changes in case mediaQueriesStringList or mediaTypesAliases changes.
+ *
+ * Current hook only offers displayMedia size, it wont expose method to change its state.
+ * @param {Array} mediaQueriesStringList - array of string media queries to be parsed
+ * @param {Array} mediaTypesAliases - array of aliases. Each value represents one mediaQueryList from array mediaQueriesStringList
+ * @param {String} defaultMediaType - default mediaTypeAlias
+ * @returns {String} current displayMedia size based on viewport size.
+ *
+ * @example Example to getDisplayMedia Size based on viewport size
+ *
+ * const displaySize = useMedia(
+ * ['(min-width: 1500px)', '(min-width: 1000px)', '(min-width: 600px)'],
+ * // Value to return for matched media query
+ * ['large', 'medium', 'small'],
+ * // Default value
+ * 'medium');
+ *
+ * const currentDisplaySize = useMedia();
*
- * @example
- * const currentViewportSize = useMedia(
- * // Media queries
- * ['(min-width: 1500px)', '(min-width: 1000px)', '(min-width: 600px)'],
- * // Value to return for matched media query
- * ['large', 'medium', 'small'],
- * // Default value
- * 'medium'
- * );
- * @param {string[]} queries
- * @param {*} values
- * @param {*} defaultValue
- * @returns
*/
-function useMedia(queries, values, defaultValue) {
- // Array containing a media query list for each query
- const mediaQueryLists = queries.map(q => window.matchMedia(q));
+const useMedia = (
+ mediaQueriesStringList,
+ mediaTypesAliases,
+ defaultMediaType
+) => {
+ // MediaQuery.state is the source of truth. This hook will be dependent on it.
+ const [state, setState] = useState(() => {
+ const _mediaQueryMap = getMediaQueryMap(mediaQueriesStringList);
+ const _displaySize = getDisplaySize(
+ _mediaQueryMap,
+ mediaTypesAliases,
+ defaultMediaType
+ );
- // Function that gets value based on matching media query
- const getValue = () => {
- // Get index of first media query that matches
- const index = mediaQueryLists.findIndex(mql => mql.matches);
+ return {
+ mediaQueryMap: _mediaQueryMap,
+ displaySize: _displaySize,
+ mediaQueriesStringList,
+ mediaTypesAliases,
+ defaultMediaType,
+ };
+ });
+ let mount = useRef(false);
- // Return related value or defaultValue if none
- return typeof values[index] !== 'undefined' ? values[index] : defaultValue;
+ const updateDisplaySize = displaySize => {
+ if (mount.current) {
+ setState({ ...state, displaySize });
+ }
};
- // State and setter for matched value
- const [value, setValue] = useState(getValue);
+ const updateState = value => {
+ const {
+ mediaQueriesStringList,
+ mediaTypesAliases,
+ defaultMediaType,
+ } = value;
- useEffect(
- () => {
- // Event listener callback
- // Note: By defining getValue outside of useEffect we ensure that it has ...
- // ... current values of hook args (as this hook callback is created once on mount).
- const handler = () => setValue(getValue);
+ const mediaQueryMap = getMediaQueryMap(mediaQueriesStringList);
+ const displaySize = getDisplaySize(
+ mediaQueryMap,
+ mediaTypesAliases,
+ defaultMediaType
+ );
+ // immutable state
+ // last chance to avoid setState of unmount component
+ if (mount.current) {
+ setState({
+ ...state,
+ mediaQueriesStringList,
+ mediaTypesAliases,
+ displaySize,
+ mediaQueryMap,
+ });
+ }
+ };
- // Set a listener for each media query with above handler as callback.
- mediaQueryLists.forEach(mql => mql.addListener(handler));
+ const onMediaQueryChange = useCallback(mediaQuery => {
+ if (mediaQuery.matches) {
+ const nextDisplaySize = getMediaTypeAlias(mediaQuery, state);
+ updateDisplaySize(nextDisplaySize);
+ }
+ }, []);
- // Remove listeners on cleanup
- return () => mediaQueryLists.forEach(mql => mql.removeListener(handler));
- },
- [] // Empty array ensures effect is only run on mount and unmount
- );
+ // update state of MediaQuery in case mediaQueriesStringList or mediaTypesAliases has changed
+ useEffect(() => {
+ const {
+ mediaQueriesStringList: _mediaQueriesStringList,
+ mediaTypesAliases: _mediaTypesAliases,
+ } = state;
+ if (
+ (mediaQueriesStringList &&
+ !isEqual(mediaQueriesStringList, _mediaQueriesStringList)) ||
+ (mediaTypesAliases && !isEqual(mediaTypesAliases, _mediaTypesAliases))
+ ) {
+ updateState({
+ mediaQueriesStringList,
+ mediaTypesAliases,
+ });
+ }
+ }, [mediaQueriesStringList, mediaTypesAliases]);
- return value;
-}
+ // re-assign window resizing listeners
+ useEffect(() => {
+ const { mediaQueryMap } = state;
+ mediaQueryMap.forEach(mql => {
+ mql.removeListener(onMediaQueryChange);
+ mql.addListener(onMediaQueryChange);
+ });
+ }, [state.mediaQueryMap]);
-export default useMedia;
+ useEffect(() => {
+ mount.current = true;
+
+ return () => {
+ mount.current = false;
+ const { mediaQueryMap } = state;
+ mediaQueryMap.forEach(mql => {
+ mql.removeListener(onMediaQueryChange);
+ });
+ };
+ }, []);
+
+ return state.displaySize;
+};
+
+export { useMedia };
diff --git a/platform/viewer/src/studylist/StudyListRoute.js b/platform/viewer/src/studylist/StudyListRoute.js
index f1c91f7b3..618c1d527 100644
--- a/platform/viewer/src/studylist/StudyListRoute.js
+++ b/platform/viewer/src/studylist/StudyListRoute.js
@@ -58,7 +58,11 @@ function StudyListRoute(props) {
const appContext = useContext(AppContext);
// ~~ RESPONSIVE
const displaySize = useMedia(
- ['(min-width: 1750px)', '(min-width: 1000px)', '(min-width: 768px)'],
+ [
+ '(min-width: 1750px)',
+ '(min-width: 1000px) and (max-width: 1749px)',
+ '(max-width: 999px)',
+ ],
['large', 'medium', 'small'],
'small'
);
@@ -257,6 +261,7 @@ function StudyListRoute(props) {
filterValues={filterValues}
onFilterChange={handleFilterChange}
studyListDateFilterNumDays={appConfig.studyListDateFilterNumDays}
+ displaySize={displaySize}
/>
{/* PAGINATION FOOTER */}