From b183e78f4729aaeb82df658f273d9faf98ad7981 Mon Sep 17 00:00:00 2001 From: Rodrigo Antinarelli Date: Thu, 19 Mar 2020 19:58:13 -0300 Subject: [PATCH] feat: ui v2 - Date Range Component (#1537) * feat: Date Range wrapper component * fix yarn issues * add calendar icon * date range css styles * date range fixes * fix propTypes warning * fix clear method, defaultProps & onChange method * update inline styles to use tailwind classes * date range documentation page * minor refactor --- platform/ui/index.js | 1 + platform/ui/package.json | 6 +- platform/ui/src/assets/icons/calendar.svg | 5 + .../ui/src/components/DateRange/DateRange.css | 98 +++++++++++ .../ui/src/components/DateRange/DateRange.jsx | 156 ++++++++++++++++++ .../ui/src/components/DateRange/DateRange.mdx | 45 +++++ platform/ui/src/components/DateRange/index.js | 3 + platform/ui/src/components/Icon/getIcon.jsx | 2 + platform/ui/src/components/Input/Input.jsx | 3 + platform/ui/src/components/index.js | 2 + platform/ui/src/views/StudyList/StudyList.js | 8 +- .../StudyList/components/StudyListFilter.js | 93 +++++++---- yarn.lock | 23 ++- 13 files changed, 407 insertions(+), 38 deletions(-) create mode 100644 platform/ui/src/assets/icons/calendar.svg create mode 100644 platform/ui/src/components/DateRange/DateRange.css create mode 100644 platform/ui/src/components/DateRange/DateRange.jsx create mode 100644 platform/ui/src/components/DateRange/DateRange.mdx create mode 100644 platform/ui/src/components/DateRange/index.js diff --git a/platform/ui/index.js b/platform/ui/index.js index 8a99ddcfa..e2b7a24b2 100644 --- a/platform/ui/index.js +++ b/platform/ui/index.js @@ -3,6 +3,7 @@ import * as utils from './src/utils/'; export { Button, ButtonGroup, + DateRange, Icon, IconButton, Svg, diff --git a/platform/ui/package.json b/platform/ui/package.json index 960c23b90..b3086fec6 100644 --- a/platform/ui/package.json +++ b/platform/ui/package.json @@ -28,17 +28,19 @@ "classnames": "^2.2.6", "date-fns": "^2.10.0", "docz": "^2.2.0", - "theme-ui": "^0.2.38", "gatsby": "2.19.24", "gatsby-plugin-postcss": "^2.1.20", "gatsby-plugin-react-svg": "^3.0.0", "gatsby-plugin-sass": "^2.1.28", "gatsby-theme-docz": "^2.2.0", + "moment": "^2.24.0", "node-sass": "^4.13.1", "prop-types": "^15.7.2", "react": "16.11.0", + "react-dates": "^21.8.0", "react-dom": "16.11.0", - "react-powerplug": "1.0.0" + "react-powerplug": "1.0.0", + "theme-ui": "^0.2.38" }, "devDependencies": { "autoprefixer": "^9.7.4", diff --git a/platform/ui/src/assets/icons/calendar.svg b/platform/ui/src/assets/icons/calendar.svg new file mode 100644 index 000000000..af83a587f --- /dev/null +++ b/platform/ui/src/assets/icons/calendar.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/platform/ui/src/components/DateRange/DateRange.css b/platform/ui/src/components/DateRange/DateRange.css new file mode 100644 index 000000000..5aa872f50 --- /dev/null +++ b/platform/ui/src/components/DateRange/DateRange.css @@ -0,0 +1,98 @@ +.DateRangePicker { +} + +/** CONTAINER STYLES **/ + +.DateRangePickerInput { + @apply bg-transparent border-0 flex; +} + +.DateRangePicker_picker { + @apply -mt-1; +} + +/** INPUT DIV STYLES **/ + +.DateInput { + background: transparent; + @apply flex flex-1 w-auto; +} + +/** INPUT FIELD COMMON STYLES **/ + +.DateInput_input { + /* used data:image as background-image because svg import with relative url didn't work */ + background-image: url('data:image/svg+xml;utf8,'); + + background-size: 14px; + background-position: 10px center; + @apply bg-no-repeat; +} +.DateInput_input { + @apply cursor-pointer border-custom-blue mt-2 bg-black shadow transition duration-300 appearance-none border-t border-l border-r border-b border-solid rounded w-full py-2 px-3 text-sm text-gray-700 leading-tight pl-8; +} +.DateInput_input:hover { + @apply border-gray-500; +} +.DateInput_input:focus { + @apply border-gray-500 outline-none; +} +/** FIRST INPUT STYLES **/ +.DateInput:first-child .DateInput_input { + @apply rounded-r-none; +} + +.DateInput:first-child .DateInput_input:hover, +.DateInput:first-child .DateInput_input:focus { + @apply relative z-10; +} + +/** SECOND INPUT STYLES **/ +.DateInput:last-child .DateInput_input { + @apply rounded-l-none; + margin-left: -1px; +} +/** ARROW STYLES **/ +.DateRangePickerInput_arrow { + @apply hidden; +} + +/* SELECT MONTH PICKER */ +.DateRangePicker_select { + @apply bg-white text-custom-dark border rounded border-custom-gray appearance-none cursor-pointer text-base pl-2 pr-5 py-1; /* NEEDED FOR ARROW DOWN */ + background-image: linear-gradient(45deg, transparent 50%, gray 50%), + linear-gradient(135deg, gray 50%, transparent 50%); + background-position: calc(100% - 11px) 11px, calc(100% - 6px) calc(11px); + background-size: 5px 5px, 5px 5px; + background-repeat: no-repeat; +} +/* CALENDAR DAYS */ +.CalendarDay { + @apply border-0 rounded-full; +} + +.CalendarDay:hover, +.CalendarDay__selected, +.CalendarDay__selected:active, +.CalendarDay__selected:hover { + @apply bg-custom-blue text-white border-0 border-custom-blue; +} + +.CalendarDay__blocked_out_of_range:hover { + @apply border-0 bg-white cursor-not-allowed text-custom-gray; +} + +.CalendarDay__selected_span { + @apply border-0 bg-custom-aquaBright; +} + +/* MONTH NAVIGATION BUTTONS */ +.DayPickerNavigation_button__horizontalDefault, +.DayPickerNavigation_button__horizontalDefault:hover { + @apply border-custom-gray text-custom-gray; + top: 24px; + padding: 3px 9px; +} +.DayPickerNavigation_svg__horizontal { + @apply fill-current; +} diff --git a/platform/ui/src/components/DateRange/DateRange.jsx b/platform/ui/src/components/DateRange/DateRange.jsx new file mode 100644 index 000000000..afc6d8f05 --- /dev/null +++ b/platform/ui/src/components/DateRange/DateRange.jsx @@ -0,0 +1,156 @@ +/** REACT DATES */ +import 'react-dates/initialize'; +import 'react-dates/lib/css/_datepicker.css'; +import { DateRangePicker, isInclusivelyBeforeDay } from 'react-dates'; +import './DateRange.css'; + +import React, { useState } from 'react'; +import PropTypes from 'prop-types'; + +import moment from 'moment'; + +const DateRange = props => { + const { onChange, startDate, endDate } = props; + const [focusedInput, setFocusedInput] = useState(null); + + const today = moment(); + const lastWeek = moment().subtract(7, 'day'); + const lastMonth = moment().subtract(1, 'month'); + + const studyDatePresets = [ + { + text: 'Today', + start: today, + end: today, + }, + { + text: 'Last 7 days', + start: lastWeek, + end: today, + }, + { + text: 'Last 30 days', + start: lastMonth, + end: today, + }, + ]; + + const renderDatePresets = () => { + return ( +
+ {studyDatePresets.map(({ text, start, end }) => { + return ( + + ); + })} +
+ ); + }; + const renderMonthElement = ({ month, onMonthSelect, onYearSelect }) => { + const renderYearsOptions = () => { + const yearsRange = 20; + const options = []; + + for (let i = 0; i < yearsRange; i++) { + const year = moment().year() - i; + options.push( + + ); + } + + return options; + }; + + renderMonthElement.propTypes = { + month: PropTypes.object, + onMonthSelect: PropTypes.func, + onYearSelect: PropTypes.func, + }; + + return ( +
+
+ +
+
+ {} + +
+
+ ); + }; + + return ( + setFocusedInput(updatedVal)} + /** OPTIONAL */ + renderCalendarInfo={renderDatePresets} + renderMonthElement={renderMonthElement} + startDatePlaceholderText={'Start Date'} + endDatePlaceholderText={'End Date'} + phrases={{ + closeDatePicker: 'Close', + clearDates: 'Clear dates', + }} + isOutsideRange={day => !isInclusivelyBeforeDay(day, moment())} + hideKeyboardShortcutsPanel={true} + numberOfMonths={1} + showClearDates={false} + anchorDirection="left" + /> + ); +}; + +DateRange.defaultProps = { + startDate: null, + endDate: null, +}; + +DateRange.propTypes = { + /** Start date moment object */ + startDate: PropTypes.object, // moment date is an object + /** End date moment object */ + endDate: PropTypes.object, // moment date is an object + /** Callback that returns on object with selected dates */ + onChange: PropTypes.func.isRequired, +}; + +export default DateRange; diff --git a/platform/ui/src/components/DateRange/DateRange.mdx b/platform/ui/src/components/DateRange/DateRange.mdx new file mode 100644 index 000000000..6b4782073 --- /dev/null +++ b/platform/ui/src/components/DateRange/DateRange.mdx @@ -0,0 +1,45 @@ +--- +name: Date Range +menu: Components +route: components/date-range +--- + +import { useState } from 'react'; +import { Playground, Props } from 'docz'; +import DateRange from '../DateRange'; + +# Date Range + +Date Range is used to select a range of dates. + +## Import + +```javascript +import { DateRange } from '@ohfi/ui'; +``` + +## Date Range + + + {() => { + const [dates, setDates] = useState({ + startDate: null, + endDate: null, + }); + return ( +
+ + setDates({ startDate, endDate }) + } + /> +
+ ); + }} +
+ +## Properties + + diff --git a/platform/ui/src/components/DateRange/index.js b/platform/ui/src/components/DateRange/index.js new file mode 100644 index 000000000..58cda0a0b --- /dev/null +++ b/platform/ui/src/components/DateRange/index.js @@ -0,0 +1,3 @@ +import DateRange from './DateRange'; + +export default DateRange; diff --git a/platform/ui/src/components/Icon/getIcon.jsx b/platform/ui/src/components/Icon/getIcon.jsx index 6bc7e6489..3fcd0daac 100644 --- a/platform/ui/src/components/Icon/getIcon.jsx +++ b/platform/ui/src/components/Icon/getIcon.jsx @@ -3,6 +3,7 @@ import React from 'react'; import arrowDown from './../../assets/icons/arrow-down.svg'; import seriesActive from './../../assets/icons/series-active.svg'; import seriesInactive from './../../assets/icons/series-inactive.svg'; +import calendar from './../../assets/icons/calendar.svg'; import cancel from './../../assets/icons/cancel.svg'; import chevronDown from './../../assets/icons/chevron-down.svg'; import chevronRight from './../../assets/icons/chevron-right.svg'; @@ -21,6 +22,7 @@ const ICONS = { 'arrow-down': arrowDown, 'series-active': seriesActive, 'series-inactive': seriesInactive, + calendar: calendar, cancel: cancel, 'chevron-down': chevronDown, 'chevron-right': chevronRight, diff --git a/platform/ui/src/components/Input/Input.jsx b/platform/ui/src/components/Input/Input.jsx index 87c03af92..6380423ca 100644 --- a/platform/ui/src/components/Input/Input.jsx +++ b/platform/ui/src/components/Input/Input.jsx @@ -20,6 +20,7 @@ const Input = ({ type = 'text', value, onChange, + onFocus, ...otherProps }) => { return ( @@ -34,6 +35,7 @@ const Input = ({ type={type} value={value} onChange={onChange} + onFocus={onFocus} {...otherProps} /> @@ -49,6 +51,7 @@ Input.propTypes = { type: PropTypes.string, value: PropTypes.any, onChange: PropTypes.func, + onFocus: PropTypes.func, }; export default Input; diff --git a/platform/ui/src/components/index.js b/platform/ui/src/components/index.js index 65e719661..827970e58 100644 --- a/platform/ui/src/components/index.js +++ b/platform/ui/src/components/index.js @@ -1,5 +1,6 @@ import Button from './Button'; import ButtonGroup from './ButtonGroup'; +import DateRange from './DateRange'; import Icon from './Icon'; import IconButton from './IconButton'; import Input from './Input'; @@ -10,6 +11,7 @@ import Typography from './Typography'; export { Button, ButtonGroup, + DateRange, Icon, IconButton, Input, diff --git a/platform/ui/src/views/StudyList/StudyList.js b/platform/ui/src/views/StudyList/StudyList.js index 652fb52ec..0a4939992 100644 --- a/platform/ui/src/views/StudyList/StudyList.js +++ b/platform/ui/src/views/StudyList/StudyList.js @@ -25,16 +25,16 @@ const filtersMeta = [ { name: 'studyDate', displayName: 'Study date', - inputType: 'text', + inputType: 'date-range', isSortable: true, - gridCol: 3, + gridCol: 5, }, { name: 'description', displayName: 'Description', inputType: 'text', isSortable: true, - gridCol: 5, + gridCol: 4, }, { name: 'modality', @@ -55,7 +55,7 @@ const filtersMeta = [ displayName: 'Instances', inputType: 'none', isSortable: false, - gridCol: 3, + gridCol: 2, }, ]; diff --git a/platform/ui/src/views/StudyList/components/StudyListFilter.js b/platform/ui/src/views/StudyList/components/StudyListFilter.js index 6a16ac90f..8f88e0e70 100644 --- a/platform/ui/src/views/StudyList/components/StudyListFilter.js +++ b/platform/ui/src/views/StudyList/components/StudyListFilter.js @@ -2,7 +2,7 @@ import React, { useState } from 'react'; import PropTypes from 'prop-types'; import classnames from 'classnames'; -import { Button, Icon, Input, Typography } from '@ohif/ui'; +import { Button, Icon, Input, Typography, DateRange } from '@ohif/ui'; const sortIconMap = { '-1': 'sorting-active-down', @@ -14,7 +14,8 @@ const defaultProps = { filtersValues: { patientName: '', mrn: '', - studyDate: '', + startDate: null, + endDate: null, description: '', modality: '', accession: '', @@ -31,7 +32,6 @@ const FilterLabel = ({ isBeingSorted = false, sortDirection = 0, onLabelClick, - inputType, className, children, }) => { @@ -70,6 +70,16 @@ const FilterLabel = ({ ); }; +FilterLabel.propTypes = { + label: PropTypes.string, + isSortable: PropTypes.bool, + isBeingSorted: PropTypes.bool, + sortDirection: PropTypes.number, + onLabelClick: PropTypes.func, + className: PropTypes.string, + children: PropTypes.node, +}; + const StudyListFilter = ({ filtersMeta = [], filtersValues = defaultProps.filtersValues, @@ -107,22 +117,49 @@ const StudyListFilter = ({ }; const clearFilters = () => { - const _filterValues = { ...currentFiltersValues }; - filtersMeta.forEach(filter => { - if (_filterValues[filter.name]) { - delete _filterValues[filter.name]; - } - }); - setcurrentFiltersValues(_filterValues); + setcurrentFiltersValues(defaultProps.filtersValues); }; const isFiltering = () => { - return filtersMeta.some(filter => { - const filterValue = currentFiltersValues[filter.name]; - return filterValue && filterValue !== ''; + return Object.keys(currentFiltersValues).some(name => { + const filterValue = currentFiltersValues[name]; + return filterValue !== defaultProps.filtersValues[name]; }); }; + const renderInput = (inputType, name) => { + switch (inputType) { + case 'date-range': { + return ( +
+ { + setcurrentFiltersValues(state => ({ + ...state, + startDate, + endDate, + })); + }} + /> +
+ ); + } + + default: + return ( + handleFilterValueChange(event, name)} + /> + ); + } + }; + return ( <>
@@ -203,17 +240,7 @@ const StudyListFilter = ({ onLabelClick={() => handleFilterLabelClick(name)} inputType={inputType} > - {inputType !== 'none' && ( - - handleFilterValueChange(event, name) - } - /> - )} + {inputType !== 'none' && renderInput(inputType, name)}
); @@ -237,15 +264,19 @@ const StudyListFilter = ({ }; StudyListFilter.propTypes = { - filtersMeta: PropTypes.arrayOf({ - name: PropTypes.string, - dsplayName: PropTypes.string, - inputType: PropTypes.oneOf(['text', 'select', 'date-range', 'none']), - isSortable: PropTypes.bool, - gridCol: PropTypes.oneOf([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]), - }), + filtersMeta: PropTypes.arrayOf( + PropTypes.shape({ + name: PropTypes.string, + dsplayName: PropTypes.string, + inputType: PropTypes.oneOf(['text', 'select', 'date-range', 'none']), + isSortable: PropTypes.bool, + gridCol: PropTypes.oneOf([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]), + }) + ), filtersValues: PropTypes.object, numOfStudies: PropTypes.number, + startDate: PropTypes.string, + endDate: PropTypes.string, }; export default StudyListFilter; diff --git a/yarn.lock b/yarn.lock index 46cea2a9a..97b23149f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16384,6 +16384,27 @@ react-dates@21.2.1: react-with-styles "^4.0.1" react-with-styles-interface-css "^6.0.0" +react-dates@^21.8.0: + version "21.8.0" + resolved "https://registry.yarnpkg.com/react-dates/-/react-dates-21.8.0.tgz#355c3c7a243a7c29568fe00aca96231e171a5e94" + integrity sha512-PPriGqi30CtzZmoHiGdhlA++YPYPYGCZrhydYmXXQ6RAvAsaONcPtYgXRTLozIOrsQ5mSo40+DiA5eOFHnZ6xw== + dependencies: + airbnb-prop-types "^2.15.0" + consolidated-events "^1.1.1 || ^2.0.0" + enzyme-shallow-equal "^1.0.0" + is-touch-device "^1.0.1" + lodash "^4.1.1" + object.assign "^4.1.0" + object.values "^1.1.0" + prop-types "^15.7.2" + raf "^3.4.1" + react-moment-proptypes "^1.6.0" + react-outside-click-handler "^1.2.4" + react-portal "^4.2.0" + react-with-direction "^1.3.1" + react-with-styles "^4.1.0" + react-with-styles-interface-css "^6.0.0" + react-dev-utils@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/react-dev-utils/-/react-dev-utils-4.2.3.tgz#5b42d9ea58d5e9e017a2f57a40a8af408a3a46fb" @@ -16766,7 +16787,7 @@ react-with-styles-interface-css@^6.0.0: array.prototype.flat "^1.2.1" global-cache "^1.2.1" -react-with-styles@^4.0.1: +react-with-styles@^4.0.1, react-with-styles@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/react-with-styles/-/react-with-styles-4.1.0.tgz#4bfc2daa92dd72033fc19fd861b90225a682a640" integrity sha512-zp05fyA6XFetqr07ox/a0bCFyEj//gUozI9cC1GW59zaGJ38STnxYvzotutgpzMyHOd7TFW9ZiZeBKjsYaS+RQ==