refactor: StudyListFilter refactor (#1580)

* Creating InputGroup

* Updating Views with new InputGroup

* Creating Docz for InputGroup

* Fixing eslint issues

* Renaming inputGroup props naming to be more generic.
This commit is contained in:
Gustavo André Lelis 2020-04-02 16:32:27 -03:00 committed by James A. Petts
parent 5766d4e91a
commit 056f56f5b4
13 changed files with 418 additions and 97 deletions

View File

@ -1,5 +1,5 @@
/** UTILS */ /** UTILS */
import * as utils from './src/utils/'; import utils from './src/utils';
export { utils }; export { utils };
/** CONTEXT/HOOKS */ /** CONTEXT/HOOKS */
@ -15,22 +15,25 @@ export {
Button, Button,
ButtonGroup, ButtonGroup,
DateRange, DateRange,
InputDateRange,
InputMultiSelect,
InputText,
InputLabelWrapper,
EmptyStudies, EmptyStudies,
Icon, Icon,
IconButton, IconButton,
Input,
InputDateRange,
InputGroup,
InputLabelWrapper,
InputMultiSelect,
InputText,
Label,
NavBar,
Select, Select,
Svg, Svg,
Input,
ThemeWrapper,
Table, Table,
TableBody, TableBody,
TableCell,
TableHead, TableHead,
TableRow, TableRow,
TableCell, ThemeWrapper,
Typography, Typography,
} from './src/components'; } from './src/components';

View File

@ -4,19 +4,14 @@ import 'react-dates/lib/css/_datepicker.css';
import { DateRangePicker, isInclusivelyBeforeDay } from 'react-dates'; import { DateRangePicker, isInclusivelyBeforeDay } from 'react-dates';
import './DateRange.css'; import './DateRange.css';
import React, { useState } from 'react'; import React, { useState, useCallback } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import moment from 'moment'; import moment from 'moment';
const DateRange = (props) => {
const { onChange, startDate, endDate } = props;
const [focusedInput, setFocusedInput] = useState(null);
const today = moment(); const today = moment();
const lastWeek = moment().subtract(7, 'day'); const lastWeek = moment().subtract(7, 'day');
const lastMonth = moment().subtract(1, 'month'); const lastMonth = moment().subtract(1, 'month');
const studyDatePresets = [ const studyDatePresets = [
{ {
text: 'Today', text: 'Today',
@ -35,6 +30,27 @@ const DateRange = (props) => {
}, },
]; ];
const renderYearsOptions = () => {
const currentYear = moment().year();
const options = [];
for (let i = 0; i < 20; i++) {
const year = currentYear - i;
options.push(
<option key={year} value={year}>
{year}
</option>
);
}
return options;
};
const DateRange = props => {
const { onChange, startDate, endDate } = props;
const [focusedInput, setFocusedInput] = useState(null);
const renderYearsOptionsCallback = useCallback(renderYearsOptions, []);
const renderDatePresets = () => { const renderDatePresets = () => {
return ( return (
<div className="PresetDateRangePicker_panel flex justify-between"> <div className="PresetDateRangePicker_panel flex justify-between">
@ -60,35 +76,30 @@ const DateRange = (props) => {
); );
}; };
const renderMonthElement = ({ month, onMonthSelect, onYearSelect }) => { 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(
<option key={year} value={year}>
{year}
</option>
);
}
return options;
};
renderMonthElement.propTypes = { renderMonthElement.propTypes = {
month: PropTypes.object, month: PropTypes.object,
onMonthSelect: PropTypes.func, onMonthSelect: PropTypes.func,
onYearSelect: PropTypes.func, onYearSelect: PropTypes.func,
}; };
const handleMonthChange = event => {
onMonthSelect(month, event.target.value);
};
const handleYearChange = event => {
onYearSelect(month, event.target.value);
};
const handleOnBlur = () => {};
return ( return (
<div className="flex justify-center"> <div className="flex justify-center">
<div className="my-0 mx-1"> <div className="my-0 mx-1">
<select <select
className="DateRangePicker_select" className="DateRangePicker_select"
value={month.month()} value={month.month()}
onChange={(e) => onMonthSelect(month, e.target.value)} onChange={handleMonthChange}
onBlur={handleOnBlur}
> >
{moment.months().map((label, value) => ( {moment.months().map((label, value) => (
<option key={value} value={value}> <option key={value} value={value}>
@ -98,13 +109,13 @@ const DateRange = (props) => {
</select> </select>
</div> </div>
<div className="my-0 mx-1"> <div className="my-0 mx-1">
{}
<select <select
className="DateRangePicker_select" className="DateRangePicker_select"
value={month.year()} value={month.year()}
onChange={(e) => onYearSelect(month, e.target.value)} onChange={handleYearChange}
onBlur={handleOnBlur}
> >
{renderYearsOptions()} {renderYearsOptionsCallback()}
</select> </select>
</div> </div>
</div> </div>
@ -120,7 +131,7 @@ const DateRange = (props) => {
endDateId={'endDateId'} endDateId={'endDateId'}
onDatesChange={onChange} onDatesChange={onChange}
focusedInput={focusedInput} focusedInput={focusedInput}
onFocusChange={(updatedVal) => setFocusedInput(updatedVal)} onFocusChange={updatedVal => setFocusedInput(updatedVal)}
/** OPTIONAL */ /** OPTIONAL */
renderCalendarInfo={renderDatePresets} renderCalendarInfo={renderDatePresets}
renderMonthElement={renderMonthElement} renderMonthElement={renderMonthElement}
@ -130,7 +141,7 @@ const DateRange = (props) => {
closeDatePicker: 'Close', closeDatePicker: 'Close',
clearDates: 'Clear dates', clearDates: 'Clear dates',
}} }}
isOutsideRange={(day) => !isInclusivelyBeforeDay(day, moment())} isOutsideRange={day => !isInclusivelyBeforeDay(day, moment())}
hideKeyboardShortcutsPanel={true} hideKeyboardShortcutsPanel={true}
numberOfMonths={1} numberOfMonths={1}
showClearDates={false} showClearDates={false}

View File

@ -0,0 +1,162 @@
import React from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import {
InputText,
InputDateRange,
InputMultiSelect,
InputLabelWrapper,
} from '@ohif/ui';
const InputGroup = ({
inputMeta,
values,
onValuesChange,
sorting,
onSortingChange,
isSortingEnable,
}) => {
const { sortBy, sortDirection } = sorting;
const handleFilterLabelClick = name => {
if (isSortingEnable) {
let _sortDirection = 'ascending';
if (sortBy === name) {
if (sortDirection === 'ascending') {
_sortDirection = 'descending';
} else if (sortDirection === 'descending') {
_sortDirection = 'none';
}
}
onSortingChange({
sortBy: _sortDirection !== 'none' ? name : '',
sortDirection: _sortDirection,
});
}
};
const renderFieldInputComponent = ({
name,
displayName,
inputProps,
isSortable,
inputType,
}) => {
const _isSortable = isSortable && isSortingEnable;
const _sortDirection = sortBy !== name ? 'none' : sortDirection;
const onLabelClick = () => {
handleFilterLabelClick(name);
};
const handleFieldChange = newValue => {
onValuesChange({
...values,
[name]: newValue,
});
};
switch (inputType) {
case 'Text':
return (
<InputText
key={name}
label={displayName}
isSortable={_isSortable}
sortDirection={_sortDirection}
onLabelClick={onLabelClick}
value={values[name]}
onChange={handleFieldChange}
/>
);
case 'MultiSelect':
return (
<InputMultiSelect
key={name}
label={displayName}
isSortable={_isSortable}
sortDirection={_sortDirection}
onLabelClick={onLabelClick}
value={values[name]}
onChange={handleFieldChange}
options={inputProps.options}
/>
);
case 'DateRange':
return (
<InputDateRange
key={name}
label={displayName}
isSortable={_isSortable}
sortDirection={_sortDirection}
onLabelClick={onLabelClick}
value={values[name]}
onChange={handleFieldChange}
/>
);
case 'None':
return (
<InputLabelWrapper
key={name}
label={displayName}
isSortable={_isSortable}
sortDirection={_sortDirection}
onLabelClick={onLabelClick}
/>
);
default:
break;
}
};
return (
<div className="container m-auto relative flex flex-col">
<div className="flex flex-row w-full">
{inputMeta.map(inputMeta => {
return (
<div
key={inputMeta.name}
className={classnames(
'pl-4 first:pl-12',
`w-${inputMeta.gridCol}/24`
)}
>
{renderFieldInputComponent(inputMeta)}
</div>
);
})}
</div>
</div>
);
};
InputGroup.propTypes = {
inputMeta: PropTypes.arrayOf(
PropTypes.shape({
name: PropTypes.string.isRequired,
displayName: PropTypes.string.isRequired,
inputType: PropTypes.oneOf(['Text', 'MultiSelect', 'DateRange', 'None'])
.isRequired,
isSortable: PropTypes.bool.isRequired,
gridCol: PropTypes.oneOf([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12])
.isRequired,
option: PropTypes.arrayOf(
PropTypes.shape({
value: PropTypes.string,
label: PropTypes.string,
})
),
})
).isRequired,
values: PropTypes.object.isRequired,
onValuesChange: PropTypes.func.isRequired,
sorting: PropTypes.shape({
sortBy: PropTypes.string,
sortDirection: PropTypes.oneOf(['ascending', 'descending', 'none']),
}).isRequired,
onSortingChange: PropTypes.func.isRequired,
isSortingEnable: PropTypes.bool.isRequired,
};
export default InputGroup;

View File

@ -0,0 +1,90 @@
---
name: InputGroup
menu: Components
route: components/InputGroup
---
import { useState } from 'react';
import { Playground, Props } from 'docz';
import { InputGroup } from '@ohif/ui';
# Input Group
## Import
```javascript
import { InputGroup } from '@ohif/ui';
```
## Basic usage
<Playground>
{() => {
const [filterValues, setFilterValues] = useState({
patient: '',
studyDate: {
startDate: null,
endDate: null,
},
modality: [],
});
const [filterSorting, setFilterSorting] = useState({
sortBy: '',
sortDirection: 'none',
});
const filtersMeta = [
{
name: 'patient',
displayName: 'Patient Name',
inputType: 'Text',
isSortable: true,
gridCol: 6,
},
{
name: 'studyDate',
displayName: 'Study date',
inputType: 'DateRange',
isSortable: true,
gridCol: 6,
},
{
name: 'modality',
displayName: 'Modality',
inputType: 'MultiSelect',
inputProps: {
options: [
{ value: 'SEG', label: 'SEG' },
{ value: 'CT', label: 'CT' },
{ value: 'MR', label: 'MR' },
{ value: 'SR', label: 'SR' },
],
},
isSortable: true,
gridCol: 6,
},
{
name: 'numberOfInstance',
displayName: 'Number of Instances',
inputType: 'None',
isSortable: true,
gridCol: 6,
},
];
return (
<div className="py-4">
<InputGroup
inputMeta={filtersMeta}
values={filterValues}
onValuesChange={setFilterValues}
sorting={filterSorting}
onSortingChange={setFilterSorting}
isSortingEnable={true}
/>
</div>
);
}}
</Playground>
## Properties
<Props of={InputGroup} />

View File

@ -0,0 +1,2 @@
import InputGroup from './InputGroup';
export default InputGroup;

View File

@ -24,9 +24,11 @@ const InputLabelWrapper = ({
return ( return (
<label className={classnames(baseLabelClassName, className)}> <label className={classnames(baseLabelClassName, className)}>
<span <span
role="button"
className={spanClassName} className={spanClassName}
onClick={onLabelClick} onClick={onLabelClick}
onKeyDown={onLabelClick} onKeyDown={onLabelClick}
tabIndex="0"
> >
{label} {label}
{isSortable && ( {isSortable && (

View File

@ -59,6 +59,7 @@ InputMultiSelect.propTypes = {
sortDirection: PropTypes.oneOf(['ascending', 'descending', 'none']) sortDirection: PropTypes.oneOf(['ascending', 'descending', 'none'])
.isRequired, .isRequired,
onLabelClick: PropTypes.func.isRequired, onLabelClick: PropTypes.func.isRequired,
onChange: PropTypes.func.isRequired,
placeholder: PropTypes.string, placeholder: PropTypes.string,
options: PropTypes.arrayOf( options: PropTypes.arrayOf(
PropTypes.shape({ PropTypes.shape({
@ -78,7 +79,6 @@ InputMultiSelect.propTypes = {
label: PropTypes.string, label: PropTypes.string,
}), }),
]), ]),
onChange: PropTypes.func.isRequired,
}; };
export default InputMultiSelect; export default InputMultiSelect;

View File

@ -6,9 +6,12 @@ import Icon from './Icon';
import IconButton from './IconButton'; import IconButton from './IconButton';
import Input from './Input'; import Input from './Input';
import InputDateRange from './InputDateRange'; import InputDateRange from './InputDateRange';
import InputGroup from './InputGroup';
import InputLabelWrapper from './InputLabelWrapper'; import InputLabelWrapper from './InputLabelWrapper';
import InputMultiSelect from './InputMultiSelect'; import InputMultiSelect from './InputMultiSelect';
import InputText from './InputText'; import InputText from './InputText';
import Label from './Label';
import NavBar from './NavBar';
import Select from './Select'; import Select from './Select';
import Svg from './Svg'; import Svg from './Svg';
import Table from './Table'; import Table from './Table';
@ -28,9 +31,12 @@ export {
IconButton, IconButton,
Input, Input,
InputDateRange, InputDateRange,
InputGroup,
InputLabelWrapper, InputLabelWrapper,
InputMultiSelect, InputMultiSelect,
InputText, InputText,
Label,
NavBar,
Select, Select,
Svg, Svg,
Table, Table,

View File

@ -39,17 +39,6 @@ const ModalProvider = ({ children, modal: Modal, service }) => {
const [options, setOptions] = useState(DEFAULT_OPTIONS); const [options, setOptions] = useState(DEFAULT_OPTIONS);
/**
* Sets the implementation of a modal service that can be used by extensions.
*
* @returns void
*/
useEffect(() => {
if (service) {
service.setServiceImplementation({ hide, show });
}
}, [hide, service, show]);
/** /**
* Show the modal and override its configuration props. * Show the modal and override its configuration props.
* *
@ -69,6 +58,17 @@ const ModalProvider = ({ children, modal: Modal, service }) => {
DEFAULT_OPTIONS, DEFAULT_OPTIONS,
]); ]);
/**
* Sets the implementation of a modal service that can be used by extensions.
*
* @returns void
*/
useEffect(() => {
if (service) {
service.setServiceImplementation({ hide, show });
}
}, [hide, service, show]);
const { const {
content: ModalContent, content: ModalContent,
contentProps, contentProps,

View File

@ -30,9 +30,12 @@ export const Sidebar = React.forwardRef((props, ref) => {
<> <>
<div className="block lg:hidden"> <div className="block lg:hidden">
<div <div
role="button"
onClick={() => props.onClick()} onClick={() => props.onClick()}
onKeyDown={() => props.onClick()}
className="fixed left-0 w-full h-full bg-black opacity-80" className="fixed left-0 w-full h-full bg-black opacity-80"
style={{ top: 81, zIndex: 99999 }} style={{ top: 81, zIndex: 99999 }}
tabIndex="0"
/> />
</div> </div>
<div <div

View File

@ -3,4 +3,8 @@ import getMockedStudies from './getMockedStudies';
import getModalities from './getModalities'; import getModalities from './getModalities';
import getInstances from './getInstances'; import getInstances from './getInstances';
const utils = { capitalize, getMockedStudies, getModalities, getInstances };
export { capitalize, getMockedStudies, getModalities, getInstances }; export { capitalize, getMockedStudies, getModalities, getInstances };
export default utils;

View File

@ -1,4 +1,4 @@
import React from 'react'; import React, { useState } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import classnames from 'classnames'; import classnames from 'classnames';
@ -67,7 +67,7 @@ const filtersMeta = [
}, },
]; ];
const filtersValues = { const defaultFilterValues = {
patientName: '', patientName: '',
mrn: '', mrn: '',
studyDate: { studyDate: {
@ -83,10 +83,18 @@ const filtersValues = {
resultsPerPage: 25, resultsPerPage: 25,
}; };
const isFiltering = (filterValues, defaultFilterValues) => {
return Object.keys(defaultFilterValues).some(name => {
return filterValues[name] !== defaultFilterValues[name];
});
};
const StudyList = ({ studies, perPage }) => { const StudyList = ({ studies, perPage }) => {
const [filterValues, setFilterValues] = useState(defaultFilterValues);
const studiesData = studies.slice(0, perPage); const studiesData = studies.slice(0, perPage);
const numOfStudies = studies.length; const numOfStudies = studies.length;
const isEmptyStudies = numOfStudies === 0; const isEmptyStudies = numOfStudies === 0;
return ( return (
<div <div
className={classnames('bg-black h-full', { className={classnames('bg-black h-full', {
@ -97,7 +105,10 @@ const StudyList = ({ studies, perPage }) => {
<StudyListFilter <StudyListFilter
numOfStudies={numOfStudies} numOfStudies={numOfStudies}
filtersMeta={filtersMeta} filtersMeta={filtersMeta}
filtersValues={filtersValues} filterValues={filterValues}
setFilterValues={setFilterValues}
clearFilters={() => setFilterValues(defaultFilterValues)}
isFiltering={isFiltering(filterValues, defaultFilterValues)}
/> />
<StudyListTable <StudyListTable
studies={studiesData} studies={studiesData}

View File

@ -1,17 +1,9 @@
import React, { useState } from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import classnames from 'classnames';
import { import { Button, Icon, Typography, InputGroup } from '@ohif/ui';
Button,
Icon,
Typography,
InputText,
InputDateRange,
InputMultiSelect,
InputLabelWrapper,
} from '@ohif/ui';
<<<<<<< HEAD
const isFiltering = (currentFiltersValues, filtersValues) => { const isFiltering = (currentFiltersValues, filtersValues) => {
return Object.keys(currentFiltersValues).some((name) => { return Object.keys(currentFiltersValues).some((name) => {
const filterValue = currentFiltersValues[name]; const filterValue = currentFiltersValues[name];
@ -129,10 +121,28 @@ const StudyListFilter = ({ filtersMeta, filtersValues, numOfStudies }) => {
default: default:
break; break;
} }
=======
const StudyListFilter = ({
filtersMeta,
filterValues,
setFilterValues,
clearFilters,
isFiltering,
numOfStudies,
}) => {
const { sortBy, sortDirection } = filterValues;
const filterSorting = { sortBy, sortDirection };
const setFilterSorting = sortingValues => {
setFilterValues({
...filterValues,
...sortingValues,
});
>>>>>>> 26d8c8ae5... refactor: StudyListFilter refactor (#1580)
}; };
const isSortingEnable = numOfStudies > 0 && numOfStudies <= 100;
return ( return (
<> <React.Fragment>
<div> <div>
<div className="bg-primary-dark"> <div className="bg-primary-dark">
<div className="container m-auto relative flex flex-col pt-5"> <div className="container m-auto relative flex flex-col pt-5">
@ -157,7 +167,7 @@ const StudyListFilter = ({ filtersMeta, filtersValues, numOfStudies }) => {
</div> </div>
</div> </div>
<div className="flex flex-row"> <div className="flex flex-row">
{isFiltering(currentFiltersValues, filtersValues) && ( {isFiltering && (
<Button <Button
rounded="full" rounded="full"
variant="outlined" variant="outlined"
@ -184,6 +194,7 @@ const StudyListFilter = ({ filtersMeta, filtersValues, numOfStudies }) => {
</div> </div>
</div> </div>
<div className="sticky z-10 border-b-4 border-black" style={{ top: 58 }}> <div className="sticky z-10 border-b-4 border-black" style={{ top: 58 }}>
<<<<<<< HEAD
<div className="bg-primary-dark pt-3 pb-3 "> <div className="bg-primary-dark pt-3 pb-3 ">
<div className="container m-auto relative flex flex-col"> <div className="container m-auto relative flex flex-col">
<div className="flex flex-row w-full"> <div className="flex flex-row w-full">
@ -202,6 +213,17 @@ const StudyListFilter = ({ filtersMeta, filtersValues, numOfStudies }) => {
})} })}
</div> </div>
</div> </div>
=======
<div className="bg-custom-navyDark pt-3 pb-3 ">
<InputGroup
inputMeta={filtersMeta}
values={filterValues}
onValuesChange={setFilterValues}
sorting={filterSorting}
onSortingChange={setFilterSorting}
isSortingEnable={isSortingEnable}
/>
>>>>>>> 26d8c8ae5... refactor: StudyListFilter refactor (#1580)
</div> </div>
{numOfStudies > 100 && ( {numOfStudies > 100 && (
<div className="container m-auto"> <div className="container m-auto">
@ -213,28 +235,33 @@ const StudyListFilter = ({ filtersMeta, filtersValues, numOfStudies }) => {
</div> </div>
)} )}
</div> </div>
</> </React.Fragment>
); );
}; };
StudyListFilter.defaultProps = {
filtersMeta: [],
filtersValues: {},
numOfStudies: 0,
};
StudyListFilter.propTypes = { StudyListFilter.propTypes = {
filtersMeta: PropTypes.arrayOf( filtersMeta: PropTypes.arrayOf(
PropTypes.shape({ PropTypes.shape({
name: PropTypes.string, name: PropTypes.string.isRequired,
dsplayName: PropTypes.string, displayName: PropTypes.string.isRequired,
inputType: PropTypes.oneOf(['Text', 'MultiSelect', 'DateRange', 'None']), inputType: PropTypes.oneOf(['Text', 'MultiSelect', 'DateRange', 'None'])
isSortable: PropTypes.bool, .isRequired,
gridCol: PropTypes.oneOf([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]), isSortable: PropTypes.bool.isRequired,
gridCol: PropTypes.oneOf([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12])
.isRequired,
option: PropTypes.arrayOf(
PropTypes.shape({
value: PropTypes.string,
label: PropTypes.string,
}) })
), ),
filtersValues: PropTypes.object, })
numOfStudies: PropTypes.number, ).isRequired,
filterValues: PropTypes.object.isRequired,
numOfStudies: PropTypes.number.isRequired,
setFilterValues: PropTypes.func.isRequired,
clearFilters: PropTypes.func.isRequired,
isFiltering: PropTypes.bool.isRequired,
}; };
export default StudyListFilter; export default StudyListFilter;