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==