From a870b3cc6056cf824af422e46f1ad674910b534e Mon Sep 17 00:00:00 2001 From: Danny Brown Date: Mon, 25 Nov 2019 10:45:37 -0500 Subject: [PATCH] fix: Issue branch from danny experimental changes pr 1128 (#1150) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: study list translations * Don't render until translations are ready * Try to wait for translations to load * Use null to render "nothing" * Try toggling useSuspense off up a layer * logging * Remove useSuspense false flags * DO NOT OPEN A PR WITH IT. Experimental changes only. Try to solve issues with useTranslation hooks #Please enter the commit message for your changes. Lines starting * Remove unecessary changes * feat: 🎸 useMedia hook to not use one more prop for upd state vl * docs: Add license scan report and status (#1161) Signed-off-by: fossabot * fix: 🐛 Fix for JS breaking on header (#1164) * feat: 🎸 Code review and refact Revised code based on PRs(variable alias,...). Changed hook for useMedia. Now, it provides two hooks: one to get displayMediaSize and other to get Entity(component, objects...) based on displayMediaSize. Implemented a similar solution for state manager to store mediaQueryList(s) and displaySize for app. * feat: 🎸 Fine tunning on mediaQuery value, fixed issue about it * chore(release): publish [skip ci] - @ohif/extension-vtk@0.53.6 - @ohif/ui@0.62.1 - @ohif/viewer@2.8.2 * Add new modal service * Change serviceManager prop to servicesManager * CR Update: fix casing and add required proptypes to providers * CR Update: Improve ohifmodal proptypes * CR Update: Fix typo in extensionmanager * CR Update: add default props to service and check service in provider * Refactor modal provider to better use its own state * ci: don't build our master branch (#1177) * ci: don't build our master branch * Add netlify-cli as a dev dependency * ci: trying a sheltered merge flow for promotions * Use modal instead of modal context * Ci/promotable builds (#1179) * ci: don't build our master branch * Add netlify-cli as a dev dependency * ci: trying a sheltered merge flow for promotions * ci: try building with QUICK_BUILD flag * Change modal children order * Ci/promotable builds (#1180) * ci: don't build our master branch * Add netlify-cli as a dev dependency * ci: trying a sheltered merge flow for promotions * ci: try building with QUICK_BUILD flag * Try using ~/repo prefix in command * Ci/promotable builds (#1181) * ci: don't build our master branch * Add netlify-cli as a dev dependency * ci: trying a sheltered merge flow for promotions * ci: try building with QUICK_BUILD flag * Try using ~/repo prefix in command * ci: make sure netlify command is available * Ci/promotable builds (#1182) * ci: don't build our master branch * Add netlify-cli as a dev dependency * ci: trying a sheltered merge flow for promotions * ci: try building with QUICK_BUILD flag * Try using ~/repo prefix in command * ci: make sure netlify command is available * ci: use sudo for global command * Fix OHIFModal proptypes * Ci/promotable builds (#1183) * ci: don't build our master branch * Add netlify-cli as a dev dependency * ci: trying a sheltered merge flow for promotions * ci: try building with QUICK_BUILD flag * Try using ~/repo prefix in command * ci: make sure netlify command is available * ci: use sudo for global command * Inline personal access token w/ env var * Ci/promotable builds (#1184) * ci: don't build our master branch * Add netlify-cli as a dev dependency * ci: trying a sheltered merge flow for promotions * ci: try building with QUICK_BUILD flag * Try using ~/repo prefix in command * ci: make sure netlify command is available * ci: use sudo for global command * Inline personal access token w/ env var * ci: workaround for sudo limitations * ci: restore release workflow (#1185) * ci: don't build our master branch * Add netlify-cli as a dev dependency * ci: trying a sheltered merge flow for promotions * ci: try building with QUICK_BUILD flag * Try using ~/repo prefix in command * ci: make sure netlify command is available * ci: use sudo for global command * Inline personal access token w/ env var * ci: workaround for sudo limitations * ci: restore release workflow * Pass services to each module, improve tests * Add servicesManager test and registerServices method * Fix key warning of snackbar elements * Remove netlify-cli; we';ll install this on CI server * Update staging and prod netlify site IDs * Clean up NPM_PUBLISH step * Clean up DOCS_PUBLISH step * Clean up Deploy workflow * Custom executor to override cypress config * Spacing * Use an existing docker hub image * Switch to npx instead of digging into npm bin location * Remove e2e test before prod deploy * Add workflow images * docs: continous integrationn * Add default props to modal * chore(release): publish [skip ci] - @ohif/extension-cornerstone@1.5.1 - @ohif/extension-vtk@0.53.7 - @ohif/core@1.9.1 - @ohif/ui@0.62.2 - @ohif/viewer@2.8.3 * chore(release): publish [skip ci] - @ohif/extension-vtk@0.53.8 - @ohif/core@1.10.0 - @ohif/ui@0.62.3 - @ohif/viewer@2.8.4 * ci: Redirect site traffic to index.html if file is not resolved * ci: fix typo * fix: 🐛 Code review. Remove 'global state' for displaySize * fix: 🐛 Code review. Ref back to useMedia and pass value down Code review. Ref back to useMedia and pass value down (components) instead of creating a specialized hook to tied any component on it * Merge from master. Missing files changes * feat: 🎸 Code review. Fix issues with DatePicker Fixed at least the minimum issue with datePicker and update some content on every translation changed --- .../ui/src/components/studyList/StudyList.js | 53 +++-- .../components/studyList/TableSearchFilter.js | 135 +++++++----- platform/ui/src/hooks/index.js | 2 +- platform/ui/src/hooks/useMedia.js | 206 ++++++++++++++---- .../viewer/src/studylist/StudyListRoute.js | 7 +- 5 files changed, 291 insertions(+), 112 deletions(-) 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 */}