Web.Common: copy FilterInput component from web.components

This commit is contained in:
Daniil Senkiv 2020-03-05 17:02:47 +03:00
parent 7a06630e34
commit 6353a8318b
12 changed files with 1492 additions and 1 deletions

View File

@ -0,0 +1,517 @@
import React from 'react';
import PropTypes from 'prop-types';
import { SearchInput } from 'asc-web-components';
import isEqual from 'lodash/isEqual';
import throttle from 'lodash/throttle';
import FilterBlock from './sub-components/FilterBlock';
import SortComboBox from './sub-components/SortComboBox';
import map from 'lodash/map';
import clone from 'lodash/clone';
import StyledFilterInput from './StyledFilterInput';
const cloneObjectsArray = function (props) {
return map(props, clone);
}
const convertToInternalData = function (fullDataArray, inputDataArray) {
const filterItems = [];
for (let i = 0; i < inputDataArray.length; i++) {
let filterValue = fullDataArray.find(x => ((x.key === inputDataArray[i].key.replace(inputDataArray[i].group + "_", '')) && x.group === inputDataArray[i].group && !x.inSubgroup));
if (filterValue) {
inputDataArray[i].key = inputDataArray[i].group + "_" + inputDataArray[i].key;
inputDataArray[i].label = filterValue.label;
inputDataArray[i].groupLabel = !fullDataArray.inSubgroup ? fullDataArray.find(x => (x.group === inputDataArray[i].group)).label : inputDataArray[i].groupLabel;
filterItems.push(inputDataArray[i]);
} else {
filterValue = fullDataArray.find(x => ((x.key === inputDataArray[i].key.replace(inputDataArray[i].group + "_", '')) && x.group === inputDataArray[i].group && x.inSubgroup));
if (filterValue) {
inputDataArray[i].key = inputDataArray[i].group + "_" + inputDataArray[i].key;
inputDataArray[i].label = filterValue.label;
inputDataArray[i].groupLabel = fullDataArray.find(x => (x.subgroup === inputDataArray[i].group)).label;
filterItems.push(inputDataArray[i]);
} else {
filterValue = fullDataArray.find(x => ((x.subgroup === inputDataArray[i].group)));
if (filterValue) {
const subgroupItems = fullDataArray.filter(t => t.group === filterValue.subgroup);
if (subgroupItems.length > 1) {
inputDataArray[i].key = inputDataArray[i].group + "_-1";
inputDataArray[i].label = filterValue.defaultSelectLabel;
inputDataArray[i].groupLabel = fullDataArray.find(x => (x.subgroup === inputDataArray[i].group)).label;
filterItems.push(inputDataArray[i]);
} else if (subgroupItems.length === 1) {
const selectFilterItem = {
key: subgroupItems[0].group + "_" + subgroupItems[0].key,
group: subgroupItems[0].group,
label: subgroupItems[0].label,
groupLabel: fullDataArray.find(x => x.subgroup === subgroupItems[0].group).label,
inSubgroup: true
};
filterItems.push(selectFilterItem);
}
}
}
}
}
return filterItems;
}
class FilterInput extends React.Component {
constructor(props) {
super(props);
this.isResizeUpdate = false;
this.minWidth = 190;
function getDefaultFilterData() {
const filterData = props.getFilterData();
const filterItems = [];
const selectedFilterData = cloneObjectsArray(props.selectedFilterData.filterValues);
selectedFilterData.forEach(defaultFilterValue => {
const filterValue = filterData.find(x => ((x.key === defaultFilterValue.key.replace(defaultFilterValue.group + "_", '')) && x.group === defaultFilterValue.group));
let groupLabel = '';
const groupFilterItem = filterData.find(x => (x.key === defaultFilterValue.group));
if (groupFilterItem != undefined) {
groupLabel = groupFilterItem.label;
} else {
const subgroupFilterItem = filterData.find(x => (x.subgroup === defaultFilterValue.group))
if (subgroupFilterItem != undefined) {
groupLabel = subgroupFilterItem.label;
}
}
if (filterValue != undefined) {
defaultFilterValue.key = defaultFilterValue.group + "_" + defaultFilterValue.key;
defaultFilterValue.label = filterValue.label;
defaultFilterValue.groupLabel = groupLabel;
filterItems.push(defaultFilterValue);
}
});
return filterItems;
}
this.state = {
sortDirection: props.selectedFilterData.sortDirection === "desc" ? true : false,
sortId: props.getSortData().findIndex(x => x.key === props.selectedFilterData.sortId) != -1 ? props.selectedFilterData.sortId : props.getSortData().length > 0 ? props.getSortData()[0].key : "",
searchText: props.selectedFilterData.inputValue || props.value,
filterValues: props.selectedFilterData ? getDefaultFilterData() : [],
openFilterItems: [],
hideFilterItems: []
};
this.searchWrapper = React.createRef();
this.filterWrapper = React.createRef();
this.onClickSortItem = this.onClickSortItem.bind(this);
this.onSortDirectionClick = this.onSortDirectionClick.bind(this);
this.onChangeSortDirection = this.onChangeSortDirection.bind(this);
this.onSearch = this.onSearch.bind(this);
this.onChangeFilter = this.onChangeFilter.bind(this);
this.onSearchChanged = this.onSearchChanged.bind(this);
this.getDefaultSelectedIndex = this.getDefaultSelectedIndex.bind(this);
this.updateFilter = this.updateFilter.bind(this);
this.onClickFilterItem = this.onClickFilterItem.bind(this);
this.getFilterData = this.getFilterData.bind(this);
this.onFilterRender = this.onFilterRender.bind(this);
this.onDeleteFilterItem = this.onDeleteFilterItem.bind(this);
this.clearFilter = this.clearFilter.bind(this);
this.throttledResize = throttle(this.resize, 300);
}
componentDidMount() {
window.addEventListener('resize', this.throttledResize);
if (this.state.filterValues.length > 0) this.updateFilter();
}
componentWillUnmount() {
window.removeEventListener('resize', this.throttledResize);
}
componentDidUpdate(prevProps){
if(this.props.needForUpdate && this.props.needForUpdate(prevProps, this.props)){
let internalFilterData = convertToInternalData(this.props.getFilterData(), cloneObjectsArray(this.props.selectedFilterData.filterValues));
this.updateFilter(internalFilterData);
}
}
shouldComponentUpdate(nextProps, nextState) {
const { selectedFilterData, getFilterData, getSortData, value, id,
isDisabled, size, placeholder } = this.props;
if (!isEqual(selectedFilterData, nextProps.selectedFilterData)) {
let internalFilterData = cloneObjectsArray(this.state.filterValues);
if (nextProps.selectedFilterData.filterValues) {
internalFilterData = convertToInternalData(getFilterData(), cloneObjectsArray(nextProps.selectedFilterData.filterValues));
this.updateFilter(internalFilterData);
}
this.setState(
{
sortDirection: nextProps.selectedFilterData.sortDirection === "desc" ? true : false,
sortId: getSortData().findIndex(x => x.key === nextProps.selectedFilterData.sortId) != -1 ? nextProps.selectedFilterData.sortId : "",
filterValues: internalFilterData,
searchText: nextProps.selectedFilterData.inputValue || value
}
);
return true;
}
if (id != nextProps.id ||
isDisabled != nextProps.isDisabled ||
size != nextProps.size ||
placeholder != nextProps.placeholder ||
value != nextProps.value)
return true;
if (this.isResizeUpdate) {
return true;
}
return !isEqual(this.state, nextState);
}
resize = () => {
this.isResizeUpdate = true;
this.setState({
filterValues: this.state.filterValues,
openFilterItems: this.state.filterValues,
hideFilterItems: []
})
}
onChangeSortDirection(key) {
this.onFilter(this.state.filterValues, this.state.sortId, key ? "desc" : "asc");
this.setState({ sortDirection: !!key });
}
getDefaultSelectedIndex() {
const sortData = this.props.getSortData();
if (sortData.length > 0) {
const defaultIndex = sortData.findIndex(x => x.key === this.state.sortId);
return defaultIndex != -1 ? defaultIndex : 0;
}
return 0;
}
onClickSortItem(key) {
this.setState({ sortId: key });
this.onFilter(this.state.filterValues, key, this.state.sortDirection ? "desc" : "asc");
}
onSortDirectionClick() {
this.onFilter(this.state.filterValues, this.state.sortId, !this.state.sortDirection ? "desc" : "asc");
this.setState({ sortDirection: !this.state.sortDirection });
}
onSearchChanged(value) {
this.setState({ searchText: value });
this.onFilter(this.state.filterValues, this.state.sortId, this.state.sortDirection ? "desc" : "asc", value);
}
onSearch(result) {
this.onFilter(result.filterValues, this.state.sortId, this.state.sortDirection ? "desc" : "asc");
}
getFilterData() {
const _this = this;
const d = this.props.getFilterData();
const result = [];
d.forEach(element => {
if (!element.inSubgroup) {
element.onClick = !element.isSeparator && !element.isHeader && !element.disabled ? ((e) => _this.props.onClickFilterItem(e, element)) : undefined;
element.key = element.group != element.key ? element.group + "_" + element.key : element.key;
if (element.subgroup != undefined) {
if (d.findIndex(x => x.group === element.subgroup) === -1) element.disabled = true;
}
result.push(element);
}
});
return result;
}
clearFilter() {
this.setState({
searchText: '',
filterValues: [],
openFilterItems: [],
hideFilterItems: []
});
this.onFilter([], this.state.sortId, this.state.sortDirection ? "desc" : "asc", '');
}
updateFilter(inputFilterItems) {
const currentFilterItems = inputFilterItems || cloneObjectsArray(this.state.filterValues);
const fullWidth = this.searchWrapper.current.getBoundingClientRect().width;
const filterWidth = this.filterWrapper.current.getBoundingClientRect().width;
const filterArr = Array.from(Array.from(this.filterWrapper.current.children).find(x => x.id === 'filter-items-container').children);
const searchFilterButton = Array.from(this.filterWrapper.current.children).find(x => x.id != 'filter-items-container');
const filterButton = searchFilterButton ? Array.from(searchFilterButton.children)[0] : null;
if (fullWidth <= this.minWidth && fullWidth > 0) {
this.setState({
openFilterItems: [],
hideFilterItems: cloneObjectsArray(currentFilterItems)
});
} else if (filterWidth > fullWidth / 2) {
let newOpenFilterItems = cloneObjectsArray(currentFilterItems);
let newHideFilterItems = [];
let elementsWidth = 0;
Array.from(filterArr).forEach(element => {
elementsWidth = elementsWidth + element.getBoundingClientRect().width;
});
if ( filterButton !== null && (elementsWidth >= (fullWidth / 3) - filterButton.getBoundingClientRect().width)) {
for (let i = 0; i < filterArr.length; i++) {
if (elementsWidth > (fullWidth / 3) - filterButton.getBoundingClientRect().width) {
elementsWidth = elementsWidth - filterArr[i].getBoundingClientRect().width;
const hiddenItem = currentFilterItems.find(x => x.key === filterArr[i].getAttribute('id'));
if (hiddenItem) newHideFilterItems.push(hiddenItem);
newOpenFilterItems.splice(newOpenFilterItems.findIndex(x => x.key === filterArr[i].getAttribute('id')), 1);
}
}
}
this.setState({
openFilterItems: newOpenFilterItems,
hideFilterItems: newHideFilterItems
});
} else {
this.setState({
openFilterItems: currentFilterItems.slice(),
hideFilterItems: []
});
}
}
onDeleteFilterItem(key) {
const currentFilterItems = this.state.filterValues.slice();
const indexFilterItem = currentFilterItems.findIndex(x => x.key === key);
if (indexFilterItem != -1) {
currentFilterItems.splice(indexFilterItem, 1);
}
this.setState({
filterValues: currentFilterItems,
openFilterItems: currentFilterItems,
hideFilterItems: []
});
let filterValues = cloneObjectsArray(currentFilterItems);
filterValues = filterValues.map(function (item) {
item.key = item.key.replace(item.group + "_", '');
return item;
})
this.onFilter(filterValues.filter(item => item.key != '-1'), this.state.sortId, this.state.sortDirection ? "desc" : "asc");
}
onFilter(filterValues, sortId, sortDirection, searchText) {
let cloneFilterValues = cloneObjectsArray(filterValues);
cloneFilterValues = cloneFilterValues.map(function (item) {
item.key = item.key.replace(item.group + "_", '');
return item;
})
this.props.onFilter({
inputValue: searchText != undefined ? searchText : this.state.searchText,
filterValues: cloneFilterValues.filter(item => item.key != '-1'),
sortId: sortId,
sortDirection: sortDirection
});
}
onChangeFilter(result) {
this.setState({
searchText: result.inputValue,
filterValues: result.filterValues,
});
this.onFilter(result.filterValues, this.state.sortId, this.state.sortDirection ? "desc" : "asc", result.inputValue);
}
onFilterRender() {
if (this.isResizeUpdate) {
this.isResizeUpdate = false;
}
if (this.searchWrapper.current && this.filterWrapper.current) {
const fullWidth = this.searchWrapper.current.getBoundingClientRect().width;
const filterWidth = this.filterWrapper.current.getBoundingClientRect().width;
if (fullWidth <= this.minWidth || filterWidth > fullWidth / 2) this.updateFilter();
}
}
onClickFilterItem(event, filterItem) {
const currentFilterItems = cloneObjectsArray(this.state.filterValues);
if (filterItem.subgroup) {
const indexFilterItem = currentFilterItems.findIndex(x => x.group === filterItem.subgroup);
if (indexFilterItem != -1) {
currentFilterItems.splice(indexFilterItem, 1);
}
const subgroupItems = this.props.getFilterData().filter(t => t.group === filterItem.subgroup);
if (subgroupItems.length > 1) {
const selectFilterItem = {
key: filterItem.subgroup + "_-1",
group: filterItem.subgroup,
label: filterItem.defaultSelectLabel,
groupLabel: filterItem.label,
inSubgroup: true
};
if (indexFilterItem != -1)
currentFilterItems.splice(indexFilterItem, 0, selectFilterItem);
else
currentFilterItems.push(selectFilterItem);
this.setState({
filterValues: currentFilterItems,
openFilterItems: currentFilterItems,
hideFilterItems: []
});
} else if (subgroupItems.length === 1) {
const selectFilterItem = {
key: subgroupItems[0].group + "_" + subgroupItems[0].key,
group: subgroupItems[0].group,
label: subgroupItems[0].label,
groupLabel: this.props.getFilterData().find(x => x.subgroup === subgroupItems[0].group).label,
inSubgroup: true
};
if (indexFilterItem != -1)
currentFilterItems.splice(indexFilterItem, 0, selectFilterItem);
else
currentFilterItems.push(selectFilterItem);
const clone = cloneObjectsArray(currentFilterItems.filter(item => item.key != '-1'));
clone.map(function (item) {
item.key = item.key.replace(item.group + "_", '');
return item;
})
this.onFilter(clone.filter(item => item.key != '-1'), this.state.sortId, this.state.sortDirection ? "desc" : "asc");
this.setState({
filterValues: currentFilterItems,
openFilterItems: currentFilterItems,
hideFilterItems: []
});
}
} else {
const filterItems = this.getFilterData();
const indexFilterItem = currentFilterItems.findIndex(x => x.group === filterItem.group);
if (indexFilterItem != -1) {
currentFilterItems.splice(indexFilterItem, 1);
}
const selectFilterItem = {
key: filterItem.key,
group: filterItem.group,
label: filterItem.label,
groupLabel: filterItem.inSubgroup ? filterItems.find(x => x.subgroup === filterItem.group).label : filterItems.find(x => x.key === filterItem.group).label
};
if (indexFilterItem != -1)
currentFilterItems.splice(indexFilterItem, 0, selectFilterItem);
else
currentFilterItems.push(selectFilterItem);
this.setState({
filterValues: currentFilterItems,
openFilterItems: currentFilterItems,
hideFilterItems: []
});
const clone = cloneObjectsArray(currentFilterItems.filter(item => item.key != '-1'));
clone.map(function (item) {
item.key = item.key.replace(item.group + "_", '');
return item;
})
this.onFilter(clone.filter(item => item.key != '-1'), this.state.sortId, this.state.sortDirection ? "desc" : "asc");
}
}
render() {
/* eslint-disable react/prop-types */
const { className, id, style, size,
isDisabled, scale, getFilterData, placeholder,
getSortData, directionAscLabel, directionDescLabel } = this.props;
/* eslint-enable react/prop-types */
const { searchText, filterValues, openFilterItems,
hideFilterItems, sortId, sortDirection} = this.state;
//console.log("FilterInput render");
let iconSize = 33;
switch (size) {
case 'base':
iconSize = 33;
break;
case 'middle':
case 'big':
case 'huge':
iconSize = 41;
break;
default:
break;
}
return (
<StyledFilterInput className={className} id={id} style={style}>
<div className='styled-search-input' ref={this.searchWrapper}>
<SearchInput
id={id}
isDisabled={isDisabled}
size={size}
scale={scale}
isNeedFilter={true}
getFilterData={getFilterData}
placeholder={placeholder}
onSearchClick={this.onSearch}
onChangeFilter={this.onChangeFilter}
value={searchText}
selectedFilterData={filterValues}
showClearButton={filterValues.length > 0}
onClearSearch={this.clearFilter}
onChange={this.onSearchChanged}
>
<div className='styled-filter-block' ref={this.filterWrapper}>
<FilterBlock
openFilterItems={openFilterItems}
hideFilterItems={hideFilterItems}
iconSize={iconSize}
getFilterData={getFilterData}
onClickFilterItem={this.onClickFilterItem}
onDeleteFilterItem={this.onDeleteFilterItem}
isResizeUpdate={this.isResizeUpdate}
onRender={this.onFilterRender}
isDisabled={isDisabled}
/>
</div>
</SearchInput>
</div>
<SortComboBox
options={getSortData()}
isDisabled={isDisabled}
onChangeSortId={this.onClickSortItem}
onChangeSortDirection={this.onChangeSortDirection}
selectedOption={getSortData().length > 0 ? getSortData().find(x => x.key === sortId) : {}}
onButtonClick={this.onSortDirectionClick}
sortDirection={+sortDirection}
directionAscLabel={directionAscLabel}
directionDescLabel={directionDescLabel}
/>
</StyledFilterInput>
);
}
}
FilterInput.protoTypes = {
size: PropTypes.oneOf(['base', 'middle', 'big', 'huge']),
autoRefresh: PropTypes.bool,
selectedFilterData: PropTypes.object,
directionAscLabel: PropTypes.string,
directionDescLabel: PropTypes.string,
className: PropTypes.string,
id: PropTypes.string,
style: PropTypes.oneOfType([PropTypes.object, PropTypes.array]),
needForUpdate: PropTypes.bool
};
FilterInput.defaultProps = {
autoRefresh: true,
selectedFilterData: {
sortDirection: false,
sortId: '',
filterValues: [],
searchText: ''
},
size: 'base',
needForUpdate: false,
directionAscLabel: 'A-Z',
directionDescLabel: 'Z-A'
};
export default FilterInput;

View File

@ -0,0 +1,110 @@
import React from 'react';
import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import { StringValue } from 'react-values';
import { withKnobs, boolean, text, select } from '@storybook/addon-knobs/react';
import withReadme from 'storybook-readme/with-readme';
import Readme from './README.md';
import FilterInput from '.';
import { Button } from 'asc-web-components';
import Section from '../../../.storybook/decorators/section';
const sizeOptions = ['base', 'middle', 'big', 'huge'];
function getData() {
return [
{ key: 'filter-status', group: 'filter-status', label: 'Status', isHeader: true },
{ key: '0', group: 'filter-status', label: 'Active' },
{ key: '1', group: 'filter-status', label: 'Disabled' },
{ key: 'filter-type', group: 'filter-type', label: 'Type', isHeader: true },
{ key: '0', group: 'filter-type', label: 'Folders' },
{ key: '1', group: 'filter-type', label: 'Employee' },
{ key: 'filter-test', group: 'filter-test', label: 'Test', isHeader: true },
{ key: '0', group: 'filter-test', label: 'test1' },
{ key: '1', group: 'filter-test', label: 'test2' },
{ key: 'filter-other', group: 'filter-other', label: 'Other', isHeader: true },
{ key: '0', group: 'filter-other', subgroup: 'filter-groups', defaultSelectLabel: 'Select', label: 'Groups' },
{ key: '0', inSubgroup: true, group: 'filter-groups', label: 'Administration'},
{ key: '1', inSubgroup: true, group: 'filter-groups', label: 'Public Relations'}
];
}
function getSortData() {
return [
{key: 'name', label: 'Name', default: true},
{key: 'surname', label: 'Surname', default: true}
];
}
class FilterStory extends React.Component {
constructor(props) {
super(props);
this.state = {
selectedFilterData: {
inputValue: "text",
filterValues: [
{key: "1", group: "filter-status"}
]
}
};
this.buttonClick = this.buttonClick.bind(this);
}
buttonClick(){
this.setState({
selectedFilterData: {
filterValues: [
{key: "-1", group: "filter-groups"}
],
sortDirection: "asc",
sortId: "surname",
inputValue: "text 123"
}
});
}
render(){
return(
<Section>
<StringValue
onChange={e => {
action('onChange')(e);
}
}
>
{({ value, set }) => (
<Section>
<div style={{marginBottom: '20px'}}>
<Button
label="Change props"
onClick={this.buttonClick}
/>
</div>
<div>
<FilterInput
id={text('id', '')}
isDisabled={boolean('isDisabled', false)}
size={select('size', sizeOptions, 'base')}
scale={boolean('scale', false)}
getFilterData={getData}
getSortData={getSortData}
placeholder={text('placeholder', 'Search')}
onFilter={(result) => {console.log(result)}}
value={value}
selectedFilterData={this.state.selectedFilterData}
onChange={e => {
set(e.target.value);
}}
/>
</div>
</Section>
)}
</StringValue>
</Section>
)
}
}
storiesOf('Components|FilterInput', module)
.addDecorator(withKnobs)
.addDecorator(withReadme(Readme))
.add('base', () => (
<FilterStory/>
));

View File

@ -0,0 +1,56 @@
import React from 'react';
import { mount } from 'enzyme';
import FilterInput from '.';
describe('<FilterInput />', () => {
it('renders without error', () => {
const wrapper = mount(
<FilterInput
getFilterData={() => [{ key: 'filter-example', group: 'filter-example', label: 'example group', isHeader: true }, { key: '0', group: 'filter-example', label: 'Test' }]}
getSortData={() => [{ key: 'name', label: 'Name' }, { key: 'surname', label: 'Surname' }]}
onFilter={jest.fn()}
/>
);
expect(wrapper).toExist();
});
it('accepts id', () => {
const wrapper = mount(
<FilterInput
getFilterData={() => [{ key: 'filter-example', group: 'filter-example', label: 'example group', isHeader: true }, { key: '0', group: 'filter-example', label: 'Test' }]}
getSortData={() => [{ key: 'name', label: 'Name' }, { key: 'surname', label: 'Surname' }]}
onFilter={jest.fn()}
id="testId"
/>
);
expect(wrapper.prop('id')).toEqual('testId');
});
it('accepts className', () => {
const wrapper = mount(
<FilterInput
getFilterData={() => [{ key: 'filter-example', group: 'filter-example', label: 'example group', isHeader: true }, { key: '0', group: 'filter-example', label: 'Test' }]}
getSortData={() => [{ key: 'name', label: 'Name' }, { key: 'surname', label: 'Surname' }]}
onFilter={jest.fn()}
className="test"
/>
);
expect(wrapper.prop('className')).toEqual('test');
});
it('accepts style', () => {
const wrapper = mount(
<FilterInput
getFilterData={() => [{ key: 'filter-example', group: 'filter-example', label: 'example group', isHeader: true }, { key: '0', group: 'filter-example', label: 'Test' }]}
getSortData={() => [{ key: 'name', label: 'Name' }, { key: 'surname', label: 'Surname' }]}
onFilter={jest.fn()}
style={{ color: 'red' }}
/>
);
expect(wrapper.getDOMNode().style).toHaveProperty('color', 'red');
});
});

View File

@ -0,0 +1,46 @@
# FilterInput
Used to filter tables
### Usage
```js
import { FilterInput } from "asc-web-common";
```
```jsx
<FilterInput
getFilterData={() => [
{
key: "filter-example",
group: "filter-example",
label: "example group",
isHeader: true
},
{ key: "0", group: "filter-example", label: "Test" }
]}
getSortData={() => [
{ key: "name", label: "Name", default: true },
{ key: "surname", label: "Surname", default: true }
]}
onFilter={result => {
console.log(result);
}}
/>
```
### Properties
| Props | Type | Required | Values | Default | Description |
| -------------------- | :------------: | :------: | :-----------------------------: | :-----: | ------------------------------------------------------------------------------------------------------ |
| `className` | `string` | - | - | - | Accepts class |
| `id` | `string` | - | - | - | Used as HTML `id` property |
| `id` | `string` | - | - | - | Accepts id |
| `isDisabled` | `bool` | - | - | `false` | Indicates that the field cannot be used (e.g not authorised, or changes not saved) |
| `onChange` | `func` | - | - | - | Called with the new value. Required when input is not read only. Parent should pass it back as `value` |
| `placeholder` | `string` | - | - | - | Placeholder text for the input |
| `scale` | `bool` | - | - | - | Indicates the input field has scale |
| `selectedFilterData` | `object` | - | - | - | Selected filter data |
| `size` | `string` | | `base`, `middle`, `big`, `huge` | `base` | Supported size of the input fields. |
| `style` | `obj`, `array` | - | - | - | Accepts css style |
| `value` | `string` | - | - | - | Value of the input |

View File

@ -0,0 +1,229 @@
import styled, { css } from 'styled-components';
import { utils } from 'asc-web-components';
const { mobile } = utils.device;
const StyledFilterInput = styled.div`
width: 100%;
min-width: 255px;
&:after {
content: " ";
display: block;
height: 0;
clear: both;
visibility: hidden;
}
.styled-search-input {
display: block;
float: left;
width: calc(100% - 140px);
@media ${mobile} {
width: calc(100% - 58px);
}
.search-input-block {
& > input {
height: 30px;
}
}
}
.styled-filter-block {
display: flex;
.filter-button {
svg {
path:not(:first-child) {
stroke: #A3A9AE;
}
}
stroke: #A3A9AE;
div:active {
svg path:first-child {
fill: #ECEEF1;
stroke: #A3A9AE;
}
}
div:first-child:hover {
svg path:not(:first-child) {
stroke: #A3A9AE;
}
}
}
}
.styled-close-button {
margin-left: 7px;
margin-top: -1px;
}
.styled-filter-block {
display: flex;
align-items: center;
}
.styled-combobox {
display: inline-block;
background: transparent;
max-width: 185px;
cursor: pointer;
vertical-align: middle;
margin-top: -2px;
> div:first-child{
width: auto;
padding-left: 4px;
}
> div:last-child{
max-width: 220px;
}
.combo-button-label {
color: #333;
}
}
.styled-filter-name {
line-height: 18px;
margin-left: 5px;
}
.styled-hide-filter {
display: inline-block;
height: 100%;
}
.dropdown-style {
position: relative;
.drop-down {
padding: 16px;
}
}
.styled-sort-combobox {
display: block;
float: left;
width: 132px;
margin-left: 8px;
@media ${mobile} {
width: 50px;
.optionalBlock ~ div:first-child{
opacity: 0
}
}
.combo-button-label {
color: #333;
}
}
`;
export const StyledFilterItem = styled.div`
display: ${props => props.block ? 'flex' : 'inline-block'};
margin-bottom: ${props => props.block ? '8px' : '0'};
position: relative;
height: 100%;
margin-right: 2px;
border: 1px solid #ECEEF1;
border-radius: 3px;
background-color: #F8F9F9;
padding-right: 22px;
font-weight: 600;
font-size: 13px;
line-height: 15px;
box-sizing: border-box;
color: #555F65;
&:last-child{
margin-bottom: 0;
}
`;
export const StyledFilterItemContent = styled.div`
display: flex;
padding: 5px 4px 2px 7px;
width: 100%;
user-select: none;
color: #333;
${props =>
props.isOpen && !props.isDisabled &&
css`
background: #ECEEF1;
`}
${props =>
!props.isDisabled &&
css`
&:active{
background: #ECEEF1;
}
`}
`;
export const StyledCloseButtonBlock = styled.div`
display: flex;
cursor: ${props =>
props.isDisabled || !props.isClickable ? "default" : "pointer"};
align-items: center;
position: absolute;
height: 100%;
width: 25px;
border-left: 1px solid #ECEEF1;
right: 0;
top: 0;
background-color: #F8F9F9;
${props =>
!props.isDisabled &&
css`
&:active{
background: #ECEEF1;
svg path:first-child {
fill: #A3A9AE;
}
}
`}
`;
export const Caret = styled.div`
width: 7px;
position: absolute;
right: 6px;
transform: ${props => (props.isOpen ? "rotate(180deg)" : "rotate(0)")};
top: ${props => (props.isOpen ? "2px" : "0")};
`;
export const StyledHideFilterButton = styled.div`
box-sizing: border-box;
display: flex;
position: relative;
align-items: center;
font-weight: 600;
font-size: 16px;
height: 100%;
border: 1px solid #eceef1;
border-radius: 3px;
background-color: #f8f9f9;
padding: 0 20px 0 9px;
margin-right: 2px;
cursor: ${props => (props.isDisabled ? "default" : "pointer")};
font-family: Open Sans;
font-style: normal;
:hover {
border-color: ${props => (props.isDisabled ? "#ECEEF1" : "#A3A9AE")};
}
:active {
background-color: ${props => (props.isDisabled ? "#F8F9F9" : "#ECEEF1")};
}
`;
export const StyledIconButton = styled.div`
transform: ${state => !state.sortDirection ? 'scale(1, -1)' : 'scale(1)'};
`;
export default StyledFilterInput;

View File

@ -0,0 +1 @@
export default from './FilterInput';

View File

@ -0,0 +1,28 @@
import React from "react";
import { IconButton } from 'asc-web-components';
import PropTypes from 'prop-types';
const CloseButton = props => {
//console.log("CloseButton render");
const { className, isDisabled, onClick } = props;
return (
<div className={`styled-close-button ${className}`}>
<IconButton
color={"#A3A9AE"}
hoverColor={"#A3A9AE"}
clickColor={"#A3A9AE"}
size={10}
iconName={'CrossIcon'}
isFill={true}
isDisabled={isDisabled}
onClick={!isDisabled ? onClick : undefined}
/>
</div>
);
};
CloseButton.propTypes = {
isDisabled: PropTypes.bool,
onClick: PropTypes.func,
className: PropTypes.string
}
export default CloseButton

View File

@ -0,0 +1,236 @@
import React from 'react';
import FilterButton from './FilterButton';
import HideFilter from './HideFilter';
import throttle from 'lodash/throttle';
import { ComboBox } from 'asc-web-components';
import CloseButton from './CloseButton';
import isEqual from 'lodash/isEqual';
import PropTypes from 'prop-types';
import { StyledFilterItem, StyledFilterItemContent, StyledCloseButtonBlock } from '../StyledFilterInput';
class FilterItem extends React.Component {
constructor(props) {
super(props);
const { id } = props;
this.state = {
id,
isOpen: false
};
}
onSelect = (option) => {
const { group, key, label, inSubgroup } = option;
this.props.onSelectFilterItem(null, {
key: group + "_" + key,
label,
group,
inSubgroup: !!inSubgroup
});
}
onClick = () => {
const { isDisabled, id, onClose } = this.props;
!isDisabled && onClose(id);
}
toggleCombobox= (e, isOpen) => this.setState({ isOpen });
render() {
const { id, isOpen } = this.state;
const { block, opened, isDisabled, groupLabel,
groupItems, label } = this.props;
return (
<StyledFilterItem key={id} id={id} block={block} opened={opened} >
<StyledFilterItemContent isDisabled={isDisabled} isOpen={isOpen}>
{groupLabel}:
{groupItems.length > 1 ?
<ComboBox
className='styled-combobox'
options={groupItems}
isDisabled={isDisabled}
onSelect={this.onSelect}
selectedOption={{
key: id,
label
}}
size='content'
scaled={false}
noBorder={true}
opened={opened}
directionX='left'
toggleAction={this.toggleCombobox}
dropDownMaxHeight={200}
></ComboBox>
: <span className='styled-filter-name'>{label}</span>
}
</StyledFilterItemContent>
<StyledCloseButtonBlock onClick={this.onClick} isDisabled={isDisabled} isClickable={true}>
<CloseButton
isDisabled={isDisabled}
onClick={this.onClick}
/>
</StyledCloseButtonBlock>
</StyledFilterItem>
);
}
}
FilterItem.propTypes = {
id: PropTypes.string,
opened: PropTypes.bool,
isDisabled: PropTypes.bool,
block: PropTypes.bool,
groupItems: PropTypes.array,
label: PropTypes.string,
groupLabel: PropTypes.string,
onClose: PropTypes.func,
onSelectFilterItem: PropTypes.func
}
class FilterBlock extends React.Component {
constructor(props) {
super(props);
const { hideFilterItems, openFilterItems } = props;
this.state = {
hideFilterItems: hideFilterItems || [],
openFilterItems: openFilterItems || []
};
this.throttledRender = throttle(this.onRender, 100);
}
componentDidUpdate() {
this.throttledRender();
}
shouldComponentUpdate(nextProps, nextState) {
const { hideFilterItems, openFilterItems } = nextProps;
if (!isEqual(this.props, nextProps)) {
if (!isEqual(this.props.hideFilterItems, hideFilterItems) || !isEqual(this.props.openFilterItems, openFilterItems)) {
this.setState({
hideFilterItems,
openFilterItems
});
return false;
}
return true;
}
if (this.props.isResizeUpdate) {
return true;
}
return !isEqual(this.state, nextState);
}
onDeleteFilterItem = (key) => {
this.props.onDeleteFilterItem(key);
}
getFilterItems = () => {
const { openFilterItems, hideFilterItems } = this.state;
const _this = this;
let result = [];
let openItems = [];
let hideItems = [];
if (openFilterItems.length > 0) {
openItems = openFilterItems.map(function (item) {
const { key, group, groupLabel, label } = item;
return <FilterItem
block={false}
isDisabled={_this.props.isDisabled}
key={key}
groupItems={_this.props.getFilterData().filter(function (t) {
return (t.group == group && t.group != t.key);
})}
onSelectFilterItem={_this.props.onClickFilterItem}
id={key}
groupLabel={groupLabel}
label={label}
opened={key.indexOf('_-1') == -1 ? false : true}
onClose={_this.onDeleteFilterItem}>
</FilterItem>
});
}
if (hideFilterItems.length > 0) {
let open = false;
let hideFilterItemsList = hideFilterItems.map(function (item) {
const { key, group, groupLabel, label } = item;
open = key.indexOf('_-1') == -1 ? false : true
return <FilterItem
block={true}
isDisabled={_this.props.isDisabled}
key={key}
groupItems={_this.props.getFilterData().filter(function (t) {
return (t.group == group && t.group != t.key);
})}
onSelectFilterItem={_this.props.onClickFilterItem}
id={key}
groupLabel={groupLabel}
opened={key.indexOf('_-1') == -1 ? false : true}
label={label}
onClose={_this.onDeleteFilterItem}>
</FilterItem>
})
hideItems.push(
<HideFilter key="hide-filter" count={hideFilterItems.length} isDisabled={this.props.isDisabled} open={open}>
{
hideFilterItemsList
}
</HideFilter>
);
}
result = hideItems.concat(openItems);
return result;
}
getData = () => {
const _this = this;
const d = this.props.getFilterData();
let result = [];
d.forEach(element => {
if (!element.inSubgroup) {
element.onClick = !element.isSeparator && !element.isHeader && !element.disabled ? ((e) => _this.props.onClickFilterItem(e, element)) : undefined;
element.key = element.group != element.key ? element.group + "_" + element.key : element.key;
if (element.subgroup != undefined) {
if (d.findIndex(x => x.group === element.subgroup) == -1) element.disabled = true;
}
result.push(element);
}
});
return result;
}
onRender = () => {
this.props.onRender();
}
render() {
const _this = this;
const filterItems = this.getFilterItems();
const filterData = this.props.getFilterData();
const { iconSize, isDisabled } = this.props;
return (
<>
<div className='styled-filter-block' ref={this.filterWrapper} id='filter-items-container'>
{filterItems}
</div>
{filterData.length > 0 && <FilterButton id='filter-button' iconSize={iconSize} getData={_this.getData} isDisabled={isDisabled} />}
</>
);
}
}
FilterBlock.propTypes = {
getFilterData: PropTypes.func,
hideFilterItems: PropTypes.array,
iconSize: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
isDisabled: PropTypes.bool,
isResizeUpdate: PropTypes.bool,
onDeleteFilterItem: PropTypes.func,
onRender: PropTypes.func,
openFilterItems: PropTypes.array,
}
export default FilterBlock;

View File

@ -0,0 +1,31 @@
import React from "react";
import { ContextMenuButton } from 'asc-web-components';
import PropTypes from 'prop-types';
class FilterButton extends React.PureComponent {
render() {
const { getData, id, isDisabled, iconSize } = this.props;
//console.log('render FilterButton)
return (
<ContextMenuButton
className='filter-button'
color='#A3A9AE'
directionY='bottom'
getData={getData}
iconName='RectangleFilterIcon'
iconOpenName='RectangleFilterClickIcon'
id={id}
isDisabled={isDisabled}
size={iconSize}
title='Actions'
></ContextMenuButton>
)
}
}
FilterButton.propTypes = {
getData: PropTypes.func,
iconSize: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
id: PropTypes.string,
isDisabled: PropTypes.bool,
}
export default FilterButton

View File

@ -0,0 +1,91 @@
import React from "react";
import { Icons, DropDown, utils } from "asc-web-components";
import PropTypes from 'prop-types';
import { Caret, StyledHideFilterButton } from '../StyledFilterInput';
const { handleAnyClick } = utils.event;
class HideFilter extends React.Component {
constructor(props) {
super(props);
this.ref = React.createRef();
this.dropDownRef = React.createRef();
this.state = {
popoverOpen: this.props.open
};
}
componentWillUnmount() {
handleAnyClick(false, this.handleClick);
}
componentDidUpdate(prevState) {
const { popoverOpen } = this.state;
if (popoverOpen !== prevState.popoverOpen) {
handleAnyClick(popoverOpen, this.handleClick);
}
}
onClick = (state, e) => {
if (!state && e && this.dropDownRef.current.contains(e.target)) {
return;
}
if (!this.props.isDisabled) {
this.setState({
popoverOpen: state
});
}
};
handleClick = e => {
this.state.popoverOpen &&
!this.dropDownRef.current.firstElementChild.contains(e.target) &&
this.onClick(false);
};
render() {
//console.log("HideFilter render");
const { isDisabled, count, children } = this.props;
const { popoverOpen } = this.state;
return (
<div
className='styled-hide-filter'
onClick={this.onClick.bind(this, !popoverOpen)}
ref={this.ref}
>
<StyledHideFilterButton
id="PopoverLegacy"
isDisabled={isDisabled}
>
{count}
<Caret isOpen={popoverOpen}>
<Icons.ExpanderDownIcon
color="#A3A9AE"
isfill={true}
size="scale"
/>
</Caret>
</StyledHideFilterButton>
<div className='dropdown-style' ref={this.dropDownRef}>
<DropDown
className="drop-down"
clickOutsideAction={this.handleClick}
manualY="8px"
open={popoverOpen}
>
{children}
</DropDown>
</div>
</div>
);
}
}
HideFilter.propTypes = {
children: PropTypes.any,
count: PropTypes.number,
isDisabled: PropTypes.bool,
open: PropTypes.bool,
}
export default HideFilter;

View File

@ -0,0 +1,145 @@
import React from 'react';
import isEqual from 'lodash/isEqual';
import { ComboBox, IconButton, DropDownItem, RadioButtonGroup } from 'asc-web-components';
import PropTypes from 'prop-types';
import { StyledIconButton } from '../StyledFilterInput';
class SortComboBox extends React.Component {
constructor(props) {
super(props);
const { sortDirection } = props;
this.state = {
sortDirection
}
this.combobox = React.createRef();
}
onButtonClick = () => {
const { onChangeSortDirection } = this.props;
const { sortDirection } = this.state;
typeof onChangeSortDirection === 'function' && onChangeSortDirection(+(sortDirection === 0 ? 1 : 0));
this.setState({
sortDirection: sortDirection === 0 ? 1 : 0
});
}
onChangeSortId = (e) => {
const { onChangeSortId } = this.props;
typeof onChangeSortId === 'function' && onChangeSortId(e.target.value);
}
onChangeSortDirection = (e) => {
const sortDirection = +e.target.value;
const { onChangeSortDirection } = this.props;
this.setState({ sortDirection });
typeof onChangeSortDirection === 'function' && onChangeSortDirection(sortDirection);
}
shouldComponentUpdate(nextProps, nextState) {
//TODO
/*const comboboxText = this.combobox.current.ref.current.children[0].children[1];
if(comboboxText.scrollWidth > Math.round(comboboxText.getBoundingClientRect().width)){
comboboxText.style.opacity = "0";
}else{
comboboxText.style.opacity = "1";
}*/
const { sortDirection } = nextProps;
if (this.props.sortDirection !== sortDirection) {
this.setState({
sortDirection
});
return true;
}
return (!isEqual(this.props, nextProps) || !isEqual(this.state, nextState));
}
render() {
const { options, directionAscLabel, directionDescLabel, isDisabled,
selectedOption } = this.props;
const { sortDirection } = this.state;
let sortArray = options.map(function (item) {
item.value = item.key
return item;
});
let sortDirectionArray = [
{ value: '0', label: directionAscLabel },
{ value: '1', label: directionDescLabel }
];
const isMobile = window.innerWidth > 375; //TODO: Make some better
const advancedOptions = (
<>
<DropDownItem noHover >
<RadioButtonGroup
fontWeight={600}
isDisabled={isDisabled}
name={'direction'}
onClick={this.onChangeSortDirection}
options={sortDirectionArray}
orientation='vertical'
selected={sortDirection.toString()}
spacing='0px'
/>
</DropDownItem>
<DropDownItem isSeparator />
<DropDownItem noHover >
<RadioButtonGroup
fontWeight={600}
isDisabled={isDisabled}
name={'sort'}
onClick={this.onChangeSortId}
options={sortArray}
orientation='vertical'
selected={selectedOption.key}
spacing='0px'
/>
</DropDownItem>
</>
);
return (
<ComboBox
advancedOptions={advancedOptions}
className='styled-sort-combobox'
directionX="right"
isDisabled={isDisabled}
options={[]}
ref={this.combobox}
scaled={true}
scaledOptions={isMobile}
selectedOption={selectedOption}
size="content"
>
<StyledIconButton sortDirection={!!sortDirection}>
<IconButton
clickColor={"#333"}
color={"#A3A9AE"}
hoverColor={"#333"}
iconName={'ZASortingIcon'}
isDisabled={isDisabled}
isFill={true}
onClick={this.onButtonClick}
size={10}
/>
</StyledIconButton>
</ComboBox>
);
}
}
SortComboBox.propTypes = {
directionAscLabel: PropTypes.string,
directionDescLabel: PropTypes.string,
isDisabled: PropTypes.bool,
onButtonClick: PropTypes.func,
onChangeSortDirection: PropTypes.func,
onChangeSortId: PropTypes.func,
sortDirection: PropTypes.number,
}
SortComboBox.defaultProps = {
isDisabled: false,
sortDirection: 0
}
export default SortComboBox;

View File

@ -12,3 +12,4 @@ export { default as Layout } from './Layout';
export { default as ProfileMenu } from './ProfileMenu';
export { default as ErrorContainer } from './ErrorContainer';
export { default as ErrorBoundary } from './ErrorBoundary';
export { default as FilterInput } from './FilterInput';