Creating combo input with label, naming and input for Text, Select and DateRange (#1553)
* Issues: OHIF-46, OHIF-47, OHIF-48
This commit is contained in:
parent
3fe3634475
commit
de484275e8
@ -15,6 +15,10 @@ export {
|
|||||||
Button,
|
Button,
|
||||||
ButtonGroup,
|
ButtonGroup,
|
||||||
DateRange,
|
DateRange,
|
||||||
|
InputDateRange,
|
||||||
|
InputMultiSelect,
|
||||||
|
InputText,
|
||||||
|
InputLabelWrapper,
|
||||||
EmptyStudies,
|
EmptyStudies,
|
||||||
Icon,
|
Icon,
|
||||||
IconButton,
|
IconButton,
|
||||||
|
|||||||
52
platform/ui/src/components/InputDateRange/InputDateRange.jsx
Normal file
52
platform/ui/src/components/InputDateRange/InputDateRange.jsx
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import { DateRange, InputLabelWrapper } from '@ohif/ui';
|
||||||
|
|
||||||
|
const InputDateRange = ({
|
||||||
|
label,
|
||||||
|
isSortable,
|
||||||
|
sortDirection,
|
||||||
|
onLabelClick,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
}) => {
|
||||||
|
const { startDate, endDate } = value;
|
||||||
|
return (
|
||||||
|
<InputLabelWrapper
|
||||||
|
label={label}
|
||||||
|
isSortable={isSortable}
|
||||||
|
sortDirection={sortDirection}
|
||||||
|
onLabelClick={onLabelClick}
|
||||||
|
>
|
||||||
|
<div className="relative">
|
||||||
|
<DateRange
|
||||||
|
startDate={startDate}
|
||||||
|
endDate={endDate}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</InputLabelWrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
InputDateRange.defaultProps = {
|
||||||
|
value: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
InputDateRange.propTypes = {
|
||||||
|
label: PropTypes.string.isRequired,
|
||||||
|
isSortable: PropTypes.bool.isRequired,
|
||||||
|
sortDirection: PropTypes.oneOf(['ascending', 'descending', 'none'])
|
||||||
|
.isRequired,
|
||||||
|
onLabelClick: PropTypes.func.isRequired,
|
||||||
|
value: PropTypes.shape({
|
||||||
|
/** Start date moment object */
|
||||||
|
startDate: PropTypes.object, // moment date is an object
|
||||||
|
/** End date moment object */
|
||||||
|
endDate: PropTypes.object, // moment date is an object
|
||||||
|
}),
|
||||||
|
onChange: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default InputDateRange;
|
||||||
43
platform/ui/src/components/InputDateRange/InputDateRange.mdx
Normal file
43
platform/ui/src/components/InputDateRange/InputDateRange.mdx
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
---
|
||||||
|
name: InputDateRange
|
||||||
|
menu: Components
|
||||||
|
route: components/InputDateRange
|
||||||
|
---
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { Playground, Props } from 'docz';
|
||||||
|
import InputDateRange from './';
|
||||||
|
|
||||||
|
# Input Date Range
|
||||||
|
|
||||||
|
## Import
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { InputDateRange } from '@ohif/ui';
|
||||||
|
```
|
||||||
|
|
||||||
|
## Basic usage
|
||||||
|
|
||||||
|
<Playground>
|
||||||
|
{() => {
|
||||||
|
const [dates, setDates] = useState({
|
||||||
|
startDate: null,
|
||||||
|
endDate: null,
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
<div className="p-4 flex flex-col items-center">
|
||||||
|
<div className="w-56">
|
||||||
|
<InputDateRange
|
||||||
|
label="Dates"
|
||||||
|
value={dates}
|
||||||
|
onChange={dates => setDates(dates)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</Playground>
|
||||||
|
|
||||||
|
## Properties
|
||||||
|
|
||||||
|
<Props of={InputDateRange} />
|
||||||
2
platform/ui/src/components/InputDateRange/index.js
Normal file
2
platform/ui/src/components/InputDateRange/index.js
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
import InputDateRange from './InputDateRange';
|
||||||
|
export default InputDateRange;
|
||||||
@ -0,0 +1,63 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import classnames from 'classnames';
|
||||||
|
|
||||||
|
import { Icon } from '@ohif/ui';
|
||||||
|
|
||||||
|
const baseLabelClassName =
|
||||||
|
'flex flex-col flex-1 text-white text-lg pl-1 select-none';
|
||||||
|
const spanClassName = 'flex flex-row items-center cursor-pointer';
|
||||||
|
const sortIconMap = {
|
||||||
|
ascending: 'sorting-active-up',
|
||||||
|
descending: 'sorting-active-down',
|
||||||
|
none: 'sorting',
|
||||||
|
};
|
||||||
|
|
||||||
|
const InputLabelWrapper = ({
|
||||||
|
label,
|
||||||
|
isSortable,
|
||||||
|
sortDirection,
|
||||||
|
onLabelClick,
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<label className={classnames(baseLabelClassName, className)}>
|
||||||
|
<span
|
||||||
|
className={spanClassName}
|
||||||
|
onClick={onLabelClick}
|
||||||
|
onKeyDown={onLabelClick}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
{isSortable && (
|
||||||
|
<Icon
|
||||||
|
name={sortIconMap[sortDirection]}
|
||||||
|
className={classnames(
|
||||||
|
'mx-2 w-2',
|
||||||
|
sortDirection !== 'none'
|
||||||
|
? 'text-custom-aquaBright'
|
||||||
|
: 'text-custom-blue'
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<span>{children}</span>
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
InputLabelWrapper.defaultProps = {
|
||||||
|
className: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
InputLabelWrapper.propTypes = {
|
||||||
|
label: PropTypes.string.isRequired,
|
||||||
|
isSortable: PropTypes.bool.isRequired,
|
||||||
|
sortDirection: PropTypes.oneOf(['ascending', 'descending', 'none'])
|
||||||
|
.isRequired,
|
||||||
|
onLabelClick: PropTypes.func.isRequired,
|
||||||
|
className: PropTypes.string,
|
||||||
|
children: PropTypes.node,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default InputLabelWrapper;
|
||||||
2
platform/ui/src/components/InputLabelWrapper/index.js
Normal file
2
platform/ui/src/components/InputLabelWrapper/index.js
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
import InputLabelWrapper from './InputLabelWrapper';
|
||||||
|
export default InputLabelWrapper;
|
||||||
@ -0,0 +1,84 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import { Select, InputLabelWrapper } from '@ohif/ui';
|
||||||
|
|
||||||
|
const InputMultiSelect = ({
|
||||||
|
label,
|
||||||
|
isSortable,
|
||||||
|
sortDirection,
|
||||||
|
onLabelClick,
|
||||||
|
value,
|
||||||
|
placeholder,
|
||||||
|
options,
|
||||||
|
onChange,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<InputLabelWrapper
|
||||||
|
label={label}
|
||||||
|
isSortable={isSortable}
|
||||||
|
sortDirection={sortDirection}
|
||||||
|
onLabelClick={onLabelClick}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
placeholder={placeholder}
|
||||||
|
className="mt-2"
|
||||||
|
options={options}
|
||||||
|
value={value}
|
||||||
|
isMulti={true}
|
||||||
|
isClearable={false}
|
||||||
|
isSearchable={false}
|
||||||
|
closeMenuOnSelect={false}
|
||||||
|
hideSelectedOptions={false}
|
||||||
|
onChange={(inputvalues, { action }) => {
|
||||||
|
switch (action) {
|
||||||
|
case 'select-option':
|
||||||
|
case 'remove-value':
|
||||||
|
case 'deselect-option':
|
||||||
|
case 'clear':
|
||||||
|
onChange(inputvalues);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</InputLabelWrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
InputMultiSelect.defaultProps = {
|
||||||
|
value: [],
|
||||||
|
placeholder: '',
|
||||||
|
options: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
InputMultiSelect.propTypes = {
|
||||||
|
label: PropTypes.string.isRequired,
|
||||||
|
isSortable: PropTypes.bool.isRequired,
|
||||||
|
sortDirection: PropTypes.oneOf(['ascending', 'descending', 'none'])
|
||||||
|
.isRequired,
|
||||||
|
onLabelClick: PropTypes.func.isRequired,
|
||||||
|
placeholder: PropTypes.string,
|
||||||
|
options: PropTypes.arrayOf(
|
||||||
|
PropTypes.shape({
|
||||||
|
value: PropTypes.string,
|
||||||
|
label: PropTypes.string,
|
||||||
|
})
|
||||||
|
),
|
||||||
|
value: PropTypes.oneOfType([
|
||||||
|
PropTypes.arrayOf(
|
||||||
|
PropTypes.shape({
|
||||||
|
value: PropTypes.string,
|
||||||
|
label: PropTypes.string,
|
||||||
|
})
|
||||||
|
),
|
||||||
|
PropTypes.shape({
|
||||||
|
value: PropTypes.string,
|
||||||
|
label: PropTypes.string,
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
onChange: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default InputMultiSelect;
|
||||||
@ -0,0 +1,48 @@
|
|||||||
|
---
|
||||||
|
name: InputMultiSelect
|
||||||
|
menu: Components
|
||||||
|
route: components/InputMultiSelect
|
||||||
|
---
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { Playground, Props } from 'docz';
|
||||||
|
import InputMultiSelect from './';
|
||||||
|
|
||||||
|
# Input Multi Select
|
||||||
|
|
||||||
|
## Import
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { InputMultiSelect } from '@ohif/ui';
|
||||||
|
```
|
||||||
|
|
||||||
|
## Basic usage
|
||||||
|
|
||||||
|
<Playground>
|
||||||
|
{() => {
|
||||||
|
const [values, setValues] = useState([]);
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col flex-1 p-4 items-center">
|
||||||
|
<div className="w-56">
|
||||||
|
<InputMultiSelect
|
||||||
|
label="Modality"
|
||||||
|
options={[
|
||||||
|
{ value: 'SEG', label: 'SEG' },
|
||||||
|
{ value: 'CT', label: 'CT' },
|
||||||
|
{ value: 'MR', label: 'MR' },
|
||||||
|
{ value: 'SR', label: 'SR' },
|
||||||
|
]}
|
||||||
|
value={values}
|
||||||
|
onChange={values => {
|
||||||
|
setValues(values);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</Playground>
|
||||||
|
|
||||||
|
## Properties
|
||||||
|
|
||||||
|
<Props of={InputMultiSelect} />
|
||||||
2
platform/ui/src/components/InputMultiSelect/index.js
Normal file
2
platform/ui/src/components/InputMultiSelect/index.js
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
import InputMultiSelect from './InputMultiSelect';
|
||||||
|
export default InputMultiSelect;
|
||||||
48
platform/ui/src/components/InputText/InputText.jsx
Normal file
48
platform/ui/src/components/InputText/InputText.jsx
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import { Input, InputLabelWrapper } from '@ohif/ui';
|
||||||
|
|
||||||
|
const InputText = ({
|
||||||
|
label,
|
||||||
|
isSortable,
|
||||||
|
sortDirection,
|
||||||
|
onLabelClick,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<InputLabelWrapper
|
||||||
|
label={label}
|
||||||
|
isSortable={isSortable}
|
||||||
|
sortDirection={sortDirection}
|
||||||
|
onLabelClick={onLabelClick}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
className="border-custom-blue mt-2 bg-black"
|
||||||
|
type="text"
|
||||||
|
containerClassName="mr-2"
|
||||||
|
value={value}
|
||||||
|
onChange={event => {
|
||||||
|
onChange(event.target.value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</InputLabelWrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
InputText.defaultProps = {
|
||||||
|
value: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
InputText.propTypes = {
|
||||||
|
label: PropTypes.string.isRequired,
|
||||||
|
isSortable: PropTypes.bool.isRequired,
|
||||||
|
sortDirection: PropTypes.oneOf(['ascending', 'descending', 'none'])
|
||||||
|
.isRequired,
|
||||||
|
onLabelClick: PropTypes.func.isRequired,
|
||||||
|
value: PropTypes.string,
|
||||||
|
onChange: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default InputText;
|
||||||
41
platform/ui/src/components/InputText/InputText.mdx
Normal file
41
platform/ui/src/components/InputText/InputText.mdx
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
---
|
||||||
|
name: InputText
|
||||||
|
menu: Components
|
||||||
|
route: components/InputText
|
||||||
|
---
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { Playground, Props } from 'docz';
|
||||||
|
import InputText from './';
|
||||||
|
|
||||||
|
# Input Text
|
||||||
|
|
||||||
|
## Import
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { InputText } from '@ohif/ui';
|
||||||
|
```
|
||||||
|
|
||||||
|
## Basic usage
|
||||||
|
|
||||||
|
<Playground>
|
||||||
|
{() => {
|
||||||
|
const [text, setText] = useState('');
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col flex-1 p-4 items-center">
|
||||||
|
<div className="w-56">
|
||||||
|
<InputText
|
||||||
|
label='Name'
|
||||||
|
value={text}
|
||||||
|
onChange={value => {setText(value)}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
|
||||||
|
</Playground>
|
||||||
|
|
||||||
|
## Properties
|
||||||
|
|
||||||
|
<Props of={InputText} />
|
||||||
2
platform/ui/src/components/InputText/index.js
Normal file
2
platform/ui/src/components/InputText/index.js
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
import InputText from './InputText';
|
||||||
|
export default InputText;
|
||||||
@ -6,12 +6,13 @@
|
|||||||
@apply border-gray-500;
|
@apply border-gray-500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.customSelect__wrapper .customSelect__control:focus {
|
.customSelect__wrapper .customSelect__control:focus,
|
||||||
|
.customSelect__wrapper .customSelect__control--menu-is-open {
|
||||||
@apply border-gray-500 outline-none;
|
@apply border-gray-500 outline-none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.customSelect__wrapper .customSelect__control--menu-is-open {
|
.customSelect--is-disabled .customSelect__control--is-disabled {
|
||||||
@apply border-gray-500 outline-none;
|
@apply pointer-events-none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.customSelect__wrapper .customSelect__indicator-separator {
|
.customSelect__wrapper .customSelect__indicator-separator {
|
||||||
@ -23,17 +24,25 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.customSelect__wrapper .customSelect__option {
|
.customSelect__wrapper .customSelect__option {
|
||||||
@apply text-black;
|
@apply text-black flex flex-row items-center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.customSelect__wrapper .customSelect__option:focus {
|
||||||
|
@apply bg-custom-blue;
|
||||||
|
}
|
||||||
|
|
||||||
|
.customSelect__wrapper .customSelect__option--is-selected {
|
||||||
|
@apply bg-transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.customSelect__wrapper .customSelect__single-value {
|
.customSelect__wrapper .customSelect__single-value {
|
||||||
@apply text-white;
|
@apply text-white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.customSelect--is-disabled .customSelect__control--is-disabled {
|
|
||||||
@apply pointer-events-none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.customSelect__wrapper.customSelect--is-disabled {
|
.customSelect__wrapper.customSelect--is-disabled {
|
||||||
@apply cursor-not-allowed pointer-events-auto;
|
@apply cursor-not-allowed pointer-events-auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.customSelect__wrapper .customSelect__value-container--is-multi {
|
||||||
|
@apply px-3 py-2 inline-block truncate;
|
||||||
|
}
|
||||||
|
|||||||
@ -1,52 +1,93 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import ReactSelect from 'react-select';
|
import ReactSelect, { components } from 'react-select';
|
||||||
|
|
||||||
import './Select.css';
|
import './Select.css';
|
||||||
|
|
||||||
|
const MultiValue = props => {
|
||||||
|
const values = props.selectProps.value;
|
||||||
|
const lastValue = values[values.length - 1];
|
||||||
|
let label = props.data.label;
|
||||||
|
if (lastValue.label !== label) {
|
||||||
|
label += ', ';
|
||||||
|
}
|
||||||
|
|
||||||
|
return <span>{label}</span>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Option = props => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<components.Option {...props}>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={props.isSelected}
|
||||||
|
className="w-6 h-6 mr-2"
|
||||||
|
onChange={e => null}
|
||||||
|
/>
|
||||||
|
<label>{props.value} </label>
|
||||||
|
</components.Option>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const Select = ({
|
const Select = ({
|
||||||
autoFocus,
|
|
||||||
className,
|
className,
|
||||||
|
closeMenuOnSelect,
|
||||||
|
hideSelectedOptions,
|
||||||
|
isClearable,
|
||||||
isDisabled,
|
isDisabled,
|
||||||
isMulti,
|
isMulti,
|
||||||
isSearchable,
|
isSearchable,
|
||||||
name,
|
|
||||||
onChange,
|
onChange,
|
||||||
options,
|
options,
|
||||||
placeholder,
|
placeholder,
|
||||||
noOptionsMessage,
|
|
||||||
value,
|
value,
|
||||||
}) => {
|
}) => {
|
||||||
|
const _components = isMulti ? { Option, MultiValue } : {};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ReactSelect
|
<ReactSelect
|
||||||
autoFocus={autoFocus}
|
|
||||||
className={classnames(
|
className={classnames(
|
||||||
className,
|
className,
|
||||||
'flex flex-col flex-1 mt-2 customSelect__wrapper'
|
'flex flex-col flex-1 customSelect__wrapper'
|
||||||
)}
|
)}
|
||||||
classNamePrefix="customSelect"
|
classNamePrefix="customSelect"
|
||||||
isDisabled={isDisabled}
|
isDisabled={isDisabled}
|
||||||
|
isClearable={isClearable}
|
||||||
isMulti={isMulti}
|
isMulti={isMulti}
|
||||||
isSearchable={isSearchable}
|
isSearchable={isSearchable}
|
||||||
name={name}
|
closeMenuOnSelect={closeMenuOnSelect}
|
||||||
onChange={onChange}
|
hideSelectedOptions={hideSelectedOptions}
|
||||||
options={options}
|
components={_components}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
noOptionsMessage={noOptionsMessage}
|
options={options}
|
||||||
value={value}
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
></ReactSelect>
|
></ReactSelect>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Select.defaultProps = {
|
||||||
|
className: '',
|
||||||
|
closeMenuOnSelect: true,
|
||||||
|
hideSelectedOptions: true,
|
||||||
|
isClearable: true,
|
||||||
|
isDisabled: false,
|
||||||
|
isMulti: false,
|
||||||
|
isSearchable: true,
|
||||||
|
};
|
||||||
|
|
||||||
Select.propTypes = {
|
Select.propTypes = {
|
||||||
autoFocus: PropTypes.bool,
|
|
||||||
className: PropTypes.string,
|
className: PropTypes.string,
|
||||||
|
closeMenuOnSelect: PropTypes.bool,
|
||||||
|
hideSelectedOptions: PropTypes.bool,
|
||||||
|
isClearable: PropTypes.bool,
|
||||||
isDisabled: PropTypes.bool,
|
isDisabled: PropTypes.bool,
|
||||||
isMulti: PropTypes.bool,
|
isMulti: PropTypes.bool,
|
||||||
isSearchable: PropTypes.bool,
|
isSearchable: PropTypes.bool,
|
||||||
name: PropTypes.string,
|
onChange: PropTypes.func.isRequired,
|
||||||
onChange: PropTypes.func,
|
|
||||||
options: PropTypes.arrayOf(
|
options: PropTypes.arrayOf(
|
||||||
PropTypes.shape({
|
PropTypes.shape({
|
||||||
value: PropTypes.string,
|
value: PropTypes.string,
|
||||||
@ -54,8 +95,18 @@ Select.propTypes = {
|
|||||||
})
|
})
|
||||||
),
|
),
|
||||||
placeholder: PropTypes.string,
|
placeholder: PropTypes.string,
|
||||||
noOptionsMessage: PropTypes.func,
|
value: PropTypes.oneOfType([
|
||||||
value: PropTypes.string,
|
PropTypes.arrayOf(
|
||||||
|
PropTypes.shape({
|
||||||
|
value: PropTypes.string,
|
||||||
|
label: PropTypes.string,
|
||||||
|
})
|
||||||
|
),
|
||||||
|
PropTypes.shape({
|
||||||
|
value: PropTypes.string,
|
||||||
|
label: PropTypes.string,
|
||||||
|
}),
|
||||||
|
]),
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Select;
|
export default Select;
|
||||||
|
|||||||
@ -19,13 +19,13 @@ import { Select } from '@ohif/ui';
|
|||||||
|
|
||||||
<Playground>
|
<Playground>
|
||||||
<div className="flex flex-col flex-1 p-4 items-center">
|
<div className="flex flex-col flex-1 p-4 items-center">
|
||||||
<div className="w-20">
|
<div className="w-56">
|
||||||
<Select
|
<Select
|
||||||
options={[
|
options={[
|
||||||
{ value: 'one', label: 'one' },
|
{ value: 'ONE', label: 'ONE' },
|
||||||
{ value: 'two', label: 'two' },
|
{ value: 'TWO', label: 'TWO' },
|
||||||
{ value: 'three', label: 'three' },
|
{ value: 'THREE', label: 'THREE' },
|
||||||
{ value: 'four', label: 'four' },
|
{ value: 'FOUR', label: 'FOUR' },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -36,14 +36,14 @@ import { Select } from '@ohif/ui';
|
|||||||
|
|
||||||
<Playground>
|
<Playground>
|
||||||
<div className="flex flex-col flex-1 p-4 items-center">
|
<div className="flex flex-col flex-1 p-4 items-center">
|
||||||
<div className="w-20">
|
<div className="w-56">
|
||||||
<Select
|
<Select
|
||||||
isDisabled
|
isDisabled
|
||||||
options={[
|
options={[
|
||||||
{ value: 'one', label: 'one' },
|
{ value: 'ONE', label: 'ONE' },
|
||||||
{ value: 'two', label: 'two' },
|
{ value: 'TWO', label: 'TWO' },
|
||||||
{ value: 'three', label: 'three' },
|
{ value: 'THREE', label: 'THREE' },
|
||||||
{ value: 'four', label: 'four' },
|
{ value: 'FOUR', label: 'FOUR' },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -54,7 +54,7 @@ import { Select } from '@ohif/ui';
|
|||||||
|
|
||||||
<Playground>
|
<Playground>
|
||||||
<div className="flex flex-col flex-1 p-4 items-center">
|
<div className="flex flex-col flex-1 p-4 items-center">
|
||||||
<div className="w-20">
|
<div className="w-56">
|
||||||
<Select
|
<Select
|
||||||
noOptionsMessage={() => {
|
noOptionsMessage={() => {
|
||||||
'No Options';
|
'No Options';
|
||||||
|
|||||||
@ -5,7 +5,10 @@ import EmptyStudies from './EmptyStudies';
|
|||||||
import Icon from './Icon';
|
import Icon from './Icon';
|
||||||
import IconButton from './IconButton';
|
import IconButton from './IconButton';
|
||||||
import Input from './Input';
|
import Input from './Input';
|
||||||
import NavBar from './NavBar';
|
import InputDateRange from './InputDateRange';
|
||||||
|
import InputLabelWrapper from './InputLabelWrapper';
|
||||||
|
import InputMultiSelect from './InputMultiSelect';
|
||||||
|
import InputText from './InputText';
|
||||||
import Select from './Select';
|
import Select from './Select';
|
||||||
import Svg from './Svg';
|
import Svg from './Svg';
|
||||||
import Table from './Table';
|
import Table from './Table';
|
||||||
@ -24,7 +27,10 @@ export {
|
|||||||
Icon,
|
Icon,
|
||||||
IconButton,
|
IconButton,
|
||||||
Input,
|
Input,
|
||||||
NavBar,
|
InputDateRange,
|
||||||
|
InputLabelWrapper,
|
||||||
|
InputMultiSelect,
|
||||||
|
InputText,
|
||||||
Select,
|
Select,
|
||||||
Svg,
|
Svg,
|
||||||
Table,
|
Table,
|
||||||
|
|||||||
@ -11,60 +11,78 @@ const filtersMeta = [
|
|||||||
{
|
{
|
||||||
name: 'patientName',
|
name: 'patientName',
|
||||||
displayName: 'Patient Name',
|
displayName: 'Patient Name',
|
||||||
inputType: 'text',
|
inputType: 'Text',
|
||||||
isSortable: true,
|
isSortable: true,
|
||||||
gridCol: 4,
|
gridCol: 4,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'mrn',
|
name: 'mrn',
|
||||||
displayName: 'MRN',
|
displayName: 'MRN',
|
||||||
inputType: 'text',
|
inputType: 'Text',
|
||||||
isSortable: true,
|
isSortable: true,
|
||||||
gridCol: 2,
|
gridCol: 2,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'studyDate',
|
name: 'studyDate',
|
||||||
displayName: 'Study date',
|
displayName: 'Study date',
|
||||||
inputType: 'date-range',
|
inputType: 'DateRange',
|
||||||
isSortable: true,
|
isSortable: true,
|
||||||
gridCol: 5,
|
gridCol: 5,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'description',
|
name: 'description',
|
||||||
displayName: 'Description',
|
displayName: 'Description',
|
||||||
inputType: 'text',
|
inputType: 'Text',
|
||||||
isSortable: true,
|
isSortable: true,
|
||||||
gridCol: 4,
|
gridCol: 4,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'modality',
|
name: 'modality',
|
||||||
displayName: 'Modality',
|
displayName: 'Modality',
|
||||||
inputType: 'select',
|
inputType: 'MultiSelect',
|
||||||
selectOptions: [
|
inputProps: {
|
||||||
{ value: 'seg', label: 'seg' },
|
options: [
|
||||||
{ value: 'ct', label: 'ct' },
|
{ value: 'SEG', label: 'SEG' },
|
||||||
{ value: 'mr', label: 'mr' },
|
{ value: 'CT', label: 'CT' },
|
||||||
{ value: 'sr', label: 'sr' },
|
{ value: 'MR', label: 'MR' },
|
||||||
],
|
{ value: 'SR', label: 'SR' },
|
||||||
|
],
|
||||||
|
},
|
||||||
isSortable: true,
|
isSortable: true,
|
||||||
gridCol: 3,
|
gridCol: 3,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'accession',
|
name: 'accession',
|
||||||
displayName: 'Accession',
|
displayName: 'Accession',
|
||||||
inputType: 'text',
|
inputType: 'Text',
|
||||||
isSortable: true,
|
isSortable: true,
|
||||||
gridCol: 4,
|
gridCol: 4,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'instances',
|
name: 'instances',
|
||||||
displayName: 'Instances',
|
displayName: 'Instances',
|
||||||
inputType: 'none',
|
inputType: 'None',
|
||||||
isSortable: false,
|
isSortable: true,
|
||||||
gridCol: 2,
|
gridCol: 2,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const filtersValues = {
|
||||||
|
patientName: '',
|
||||||
|
mrn: '',
|
||||||
|
studyDate: {
|
||||||
|
startDate: null,
|
||||||
|
endDate: null,
|
||||||
|
},
|
||||||
|
description: '',
|
||||||
|
modality: undefined,
|
||||||
|
accession: '',
|
||||||
|
sortBy: '',
|
||||||
|
sortDirection: 'none',
|
||||||
|
page: 0,
|
||||||
|
resultsPerPage: 25,
|
||||||
|
};
|
||||||
|
|
||||||
const StudyList = ({ studies, perPage }) => {
|
const StudyList = ({ studies, perPage }) => {
|
||||||
const studiesData = studies.slice(0, perPage);
|
const studiesData = studies.slice(0, perPage);
|
||||||
const numOfStudies = studies.length;
|
const numOfStudies = studies.length;
|
||||||
@ -76,7 +94,11 @@ const StudyList = ({ studies, perPage }) => {
|
|||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<Header />
|
<Header />
|
||||||
<StudyListFilter numOfStudies={numOfStudies} filtersMeta={filtersMeta} />
|
<StudyListFilter
|
||||||
|
numOfStudies={numOfStudies}
|
||||||
|
filtersMeta={filtersMeta}
|
||||||
|
filtersValues={filtersValues}
|
||||||
|
/>
|
||||||
<StudyListTable
|
<StudyListTable
|
||||||
studies={studiesData}
|
studies={studiesData}
|
||||||
numOfStudies={numOfStudies}
|
numOfStudies={numOfStudies}
|
||||||
|
|||||||
@ -2,100 +2,36 @@ import React, { useState } from 'react';
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
|
|
||||||
import { Button, Icon, Input, Typography, Select, DateRange } from '@ohif/ui';
|
import {
|
||||||
|
Button,
|
||||||
|
Icon,
|
||||||
|
Typography,
|
||||||
|
InputText,
|
||||||
|
InputDateRange,
|
||||||
|
InputMultiSelect,
|
||||||
|
InputLabelWrapper,
|
||||||
|
} from '@ohif/ui';
|
||||||
|
|
||||||
const sortIconMap = {
|
const isFiltering = (currentFiltersValues, filtersValues) => {
|
||||||
'-1': 'sorting-active-down',
|
return Object.keys(currentFiltersValues).some(name => {
|
||||||
0: 'sorting',
|
const filterValue = currentFiltersValues[name];
|
||||||
1: 'sorting-active-up',
|
return filterValue !== filtersValues[name];
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultProps = {
|
const StudyListFilter = ({ filtersMeta, filtersValues, numOfStudies }) => {
|
||||||
filtersValues: {
|
|
||||||
patientName: '',
|
|
||||||
mrn: '',
|
|
||||||
startDate: null,
|
|
||||||
endDate: null,
|
|
||||||
description: '',
|
|
||||||
modality: '',
|
|
||||||
accession: '',
|
|
||||||
sortBy: '',
|
|
||||||
sortDirection: 0,
|
|
||||||
page: 0,
|
|
||||||
resultsPerPage: 25,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const FilterLabel = ({
|
|
||||||
label = '',
|
|
||||||
isSortable = false,
|
|
||||||
isBeingSorted = false,
|
|
||||||
sortDirection = 0,
|
|
||||||
onLabelClick,
|
|
||||||
className,
|
|
||||||
children,
|
|
||||||
}) => {
|
|
||||||
const handleLabelClick = () => {
|
|
||||||
if (onLabelClick) {
|
|
||||||
onLabelClick();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const iconProps = {
|
|
||||||
name: isBeingSorted ? sortIconMap[sortDirection] : 'sorting',
|
|
||||||
colorClass: isBeingSorted ? 'text-custom-aquaBright' : 'text-custom-blue',
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<label
|
|
||||||
className={classnames(
|
|
||||||
'flex flex-col flex-1 text-white text-lg pl-1 select-none',
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
className="flex flex-row items-center cursor-pointer"
|
|
||||||
onClick={handleLabelClick}
|
|
||||||
>
|
|
||||||
{label}
|
|
||||||
{isSortable && (
|
|
||||||
<Icon
|
|
||||||
name={iconProps.name}
|
|
||||||
className={classnames('mx-2 w-2', iconProps.colorClass)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
<span>{children}</span>
|
|
||||||
</label>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
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,
|
|
||||||
numOfStudies = 90,
|
|
||||||
}) => {
|
|
||||||
const [currentFiltersValues, setcurrentFiltersValues] = useState(
|
const [currentFiltersValues, setcurrentFiltersValues] = useState(
|
||||||
filtersValues
|
filtersValues
|
||||||
);
|
);
|
||||||
const { sortBy, sortDirection } = currentFiltersValues;
|
const { sortBy, sortDirection } = currentFiltersValues;
|
||||||
|
|
||||||
const handleFilterLabelClick = name => {
|
const handleFilterLabelClick = name => {
|
||||||
let _sortDirection = 1;
|
let _sortDirection = 'ascending';
|
||||||
if (sortBy === name) {
|
if (sortBy === name) {
|
||||||
_sortDirection = sortDirection + 1;
|
if (sortDirection === 'ascending') {
|
||||||
if (_sortDirection > 1) {
|
_sortDirection = 'descending';
|
||||||
_sortDirection = -1;
|
} else if (sortDirection === 'descending') {
|
||||||
|
_sortDirection = 'none';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -108,58 +44,88 @@ const StudyListFilter = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFilterValueChange = (event, name) => {
|
|
||||||
const { value } = event.target;
|
|
||||||
setcurrentFiltersValues(prevState => ({
|
|
||||||
...prevState,
|
|
||||||
[name]: value,
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
const clearFilters = () => {
|
const clearFilters = () => {
|
||||||
setcurrentFiltersValues(defaultProps.filtersValues);
|
setcurrentFiltersValues(filtersValues);
|
||||||
};
|
};
|
||||||
|
|
||||||
const isFiltering = () => {
|
const renderFieldInputComponent = ({
|
||||||
return Object.keys(currentFiltersValues).some(name => {
|
name,
|
||||||
const filterValue = currentFiltersValues[name];
|
displayName,
|
||||||
return filterValue !== defaultProps.filtersValues[name];
|
inputProps,
|
||||||
});
|
isSortable,
|
||||||
};
|
inputType,
|
||||||
|
}) => {
|
||||||
const renderInput = (inputType, name, { selectOptions }) => {
|
const _isSortable = isSortable && numOfStudies <= 100 && numOfStudies > 0;
|
||||||
|
const _sortDirection = sortBy !== name ? 'none' : sortDirection;
|
||||||
|
const onLabelClick = () => {
|
||||||
|
if (_isSortable) {
|
||||||
|
handleFilterLabelClick(name);
|
||||||
|
}
|
||||||
|
};
|
||||||
switch (inputType) {
|
switch (inputType) {
|
||||||
case 'date-range': {
|
case 'Text':
|
||||||
return (
|
return (
|
||||||
<div className="relative">
|
<InputText
|
||||||
<DateRange
|
key={name}
|
||||||
startDate={currentFiltersValues.startDate}
|
label={displayName}
|
||||||
endDate={currentFiltersValues.endDate}
|
isSortable={_isSortable}
|
||||||
onChange={({ startDate, endDate, preset = false }) => {
|
sortDirection={_sortDirection}
|
||||||
setcurrentFiltersValues(state => ({
|
onLabelClick={onLabelClick}
|
||||||
...state,
|
value={currentFiltersValues[name]}
|
||||||
startDate,
|
onChange={newValue => {
|
||||||
endDate,
|
setcurrentFiltersValues(prevState => ({
|
||||||
}));
|
...prevState,
|
||||||
}}
|
[name]: newValue,
|
||||||
/>
|
}));
|
||||||
</div>
|
}}
|
||||||
);
|
/>
|
||||||
}
|
);
|
||||||
case 'select': {
|
break;
|
||||||
return <Select options={selectOptions}></Select>;
|
case 'MultiSelect':
|
||||||
}
|
return (
|
||||||
case 'text': {
|
<InputMultiSelect
|
||||||
return (
|
key={name}
|
||||||
<Input
|
label={displayName}
|
||||||
className="border-custom-blue mt-2 bg-black"
|
isSortable={_isSortable}
|
||||||
type="text"
|
sortDirection={_sortDirection}
|
||||||
containerClassName="mr-2"
|
onLabelClick={onLabelClick}
|
||||||
value={currentFiltersValues[name] || ''}
|
options={inputProps.options}
|
||||||
onChange={event => handleFilterValueChange(event, name)}
|
value={currentFiltersValues[name]}
|
||||||
|
onChange={newValue => {
|
||||||
|
setcurrentFiltersValues(prevState => ({
|
||||||
|
...prevState,
|
||||||
|
[name]: newValue,
|
||||||
|
}));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case 'DateRange':
|
||||||
|
return (
|
||||||
|
<InputDateRange
|
||||||
|
key={name}
|
||||||
|
label={displayName}
|
||||||
|
isSortable={_isSortable}
|
||||||
|
sortDirection={_sortDirection}
|
||||||
|
onLabelClick={onLabelClick}
|
||||||
|
value={currentFiltersValues[name]}
|
||||||
|
onChange={newValue => {
|
||||||
|
setcurrentFiltersValues(prevState => ({
|
||||||
|
...prevState,
|
||||||
|
[name]: newValue,
|
||||||
|
}));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case 'None':
|
||||||
|
return (
|
||||||
|
<InputLabelWrapper
|
||||||
|
key={name}
|
||||||
|
label={displayName}
|
||||||
|
isSortable={_isSortable}
|
||||||
|
sortDirection={_sortDirection}
|
||||||
|
onLabelClick={onLabelClick}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -194,7 +160,7 @@ const StudyListFilter = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-row">
|
<div className="flex flex-row">
|
||||||
{isFiltering() && (
|
{isFiltering(currentFiltersValues, filtersValues) && (
|
||||||
<Button
|
<Button
|
||||||
rounded="full"
|
rounded="full"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
@ -224,40 +190,19 @@ const StudyListFilter = ({
|
|||||||
<div className="bg-custom-navyDark pt-3 pb-3 ">
|
<div className="bg-custom-navyDark 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">
|
||||||
{filtersMeta.map(
|
{filtersMeta.map(filterMeta => {
|
||||||
({
|
return (
|
||||||
name,
|
<div
|
||||||
displayName,
|
key={filterMeta.name}
|
||||||
inputType,
|
className={classnames(
|
||||||
isSortable,
|
'pl-4 first:pl-12',
|
||||||
gridCol,
|
`w-${filterMeta.gridCol}/24`
|
||||||
selectOptions,
|
)}
|
||||||
}) => {
|
>
|
||||||
return (
|
{renderFieldInputComponent(filterMeta)}
|
||||||
<div
|
</div>
|
||||||
key={name}
|
);
|
||||||
className={classnames(
|
})}
|
||||||
`w-${gridCol}/24`,
|
|
||||||
'pl-4 first:pl-12'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<FilterLabel
|
|
||||||
key={name}
|
|
||||||
label={displayName}
|
|
||||||
isSortable={
|
|
||||||
isSortable && numOfStudies <= 100 && numOfStudies > 0
|
|
||||||
}
|
|
||||||
isBeingSorted={sortBy === name}
|
|
||||||
sortDirection={sortDirection}
|
|
||||||
onLabelClick={() => handleFilterLabelClick(name)}
|
|
||||||
inputType={inputType}
|
|
||||||
>
|
|
||||||
{renderInput(inputType, name, { selectOptions })}
|
|
||||||
</FilterLabel>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -275,20 +220,24 @@ const StudyListFilter = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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,
|
||||||
dsplayName: PropTypes.string,
|
dsplayName: PropTypes.string,
|
||||||
inputType: PropTypes.oneOf(['text', 'select', 'date-range', 'none']),
|
inputType: PropTypes.oneOf(['Text', 'MultiSelect', 'DateRange', 'None']),
|
||||||
isSortable: PropTypes.bool,
|
isSortable: PropTypes.bool,
|
||||||
gridCol: PropTypes.oneOf([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]),
|
gridCol: PropTypes.oneOf([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]),
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
filtersValues: PropTypes.object,
|
filtersValues: PropTypes.object,
|
||||||
numOfStudies: PropTypes.number,
|
numOfStudies: PropTypes.number,
|
||||||
startDate: PropTypes.string,
|
|
||||||
endDate: PropTypes.string,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default StudyListFilter;
|
export default StudyListFilter;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user